[ Back to Kevin's Homepage ]

Ambiguous arguments

Problem:

Ambiguity leads to different behaviour depending on the state of the filesystem.

Example

$ touch src
$ mv src dst
What do you expect to happen? The result depends on what 'dst' is.
  1. If 'dst' does not exist, 'src' will be renamed 'dst'
  2. If 'dst' is a file, 'src' will be renamed 'dst', deleting the existing file
  3. If 'dst' is a directory, the file 'src' will be renamed to 'dst/src'
  4. If 'dst/src' is a directory, an error will occur

Note that there are 3 different outcomes. This makes it difficult to write scripts that work reliably. This gets even worse when 'src' is a directory, as demonstrated here:

$ rm -rf dst
$ mkdir src
$ mv src dst
$ mkdir src
$ mv src dst

If 'dst' doesn't exist, the first mv command will rename 'src' to 'dst'. The second mv command will create dst/src ! If you replace 'mkdir' with a command that creates a tree of files (like wget --mirror) suddenly instead of having a newer version, you now have two copies. The sharp edge here is that the second mv succeeds, which can trick you into thinking that it did case #1 above, not #3. Only if you run 'mkdir;mv' a third time, it will result in an error.

Solving

Notice that whenever you run ls -al' you see two entries: . and .. ? If you use these as your destination, your command becomes unambiguous. For example:

$ mkdir src
$ mv src dst/.
  1. If 'dst' does not exist, an error will occur
  2. If 'dst' is a file, an error will occur
  3. If 'dst' is a directory and dst/src is a file, an error will occur ("cannot overwrite directory ... with non-directory")
  4. If 'dst' is a directory, it will create the directory dst/src/
As you can see, now there are only two possible outcomes: an error, or successfully moving the directory. However, this precludes you from renaming your directory. One way is to not move the directory, but the contents. More commonly, you are working with trees of files with the rsync directory:
$ update my tree of files in src/...
$ rsync -n --archive --delete src/. dst/.

Since both src and dst arguments end in "/." it's completely unambiguous; you're making one directory tree mimic another. It will correctly grab all dot files or anything with a weird name; no need to try and create a glob for files like ".foo.rc". The src is guaranteed to be a directory, dst is guaranteed to be a directory. Zero ambiguity.

Two small caveats:
  1. You may need to add a mkdir to create your target directory.
  2. You can't mv a directory specified by ".". However, you can specify the source by name e.g. 'src' without the trailing "/.". Mostly it is the destination directory that requires "/." to be unambiguous.
  3. Renaming files or directories is not possible. Rename the file/directory before you move it, or instead respecify and move the contents

Conclusion

I hope that helps you write more reliable/predictable scripts.
KJW - kjw@rightsock.com