A shell script mini-clinic

On one of the mailing lists I subscribe to, someone posted the following Unix shell script as a wrapper for a program which unfortunately littered the current directory with temporary files which it did not remove. (Because this post is not about the faults of that program, I've replaced the reference to it in the fourth line of the script).

pushd . > /dev/null
cd /tmp
some-program $@
popd > /dev/null

The author added, "I'm sure there's a better way to write the script, but this would do the trick." So it does. However, the code exhibits a number of misunderstandings of how shell scripts work that I think are worth clarifying.

The first and fifth lines are used to preserve and restore the current working directory. However, a script (or any Unix process) always has its own working directory; changing the working directory in a script does not affect the caller of the script in any way. This is not true for shell startup scripts like .login, .profile, and .bashrc, or for Windows .bat and .cmd files, all of which should be careful not to permanently change the current working directory.

So the first improvement is to remove the first and fifth lines entirely. Since these are the only parts dependent on the particular shell being run, namely bash, it is now possible to change the first line to read #!/bin/sh. On Linux and Cygwin, this is a distinction without a difference, but on Solaris and BSD Unixes, sh is a different shell from bash, and less of a resource hog; on older operating system versions, bash may not even be present. Therefore, it's always best practice to write simple shell scripts like this one as portably as possible, running them with sh whenever you can. (Of course, there is no reason to avoid bash-specific features in full-fledged bash programs, where you are using bash as a programming language like Perl or Python.)

Lastly, using $@ to mean "all the arguments" is unsafe if any of the arguments might contain a space character. Instead, use "$@". For the same reason, $* should be avoided entirely, unless you want to reparse the arguments according to whatever whitespace they contain. The distinction doesn't happen to matter in this script, but it's a very good habit to get into in general, because some day you will have to process a file (possibly coming from Windows or the Mac) with a space in its name, and then your script will break embarrassingly.


Marcos said...

I knew you were into languages, John, but I didn't know Shellscript was one of them. :)

Although you're unlikely to run into it anymore at this late date, some older versions of Bourne don't handle "$@" properly if there are no arguments, passing the empty string instead of no args at all. The solution there is to use this little trick:

some-program ${1+"$@"}

Also, if I ever do need to temporarily switch directories within a single script for some reason, I tend to use a subshell (as long as I don't need any shell environment side-effects to linger afterward):

cd some-dir
do some stuff
# now we're back where we were

Anonymous said...

And if some-program is the last line of the shell script, you could exec it, saving a fork and making your pstree listings marginally cleaner.

Anonymous said...

My fav:

$ pwd
$ (cd /tmp; litter_tool make a mess *.txt of it)
$ ls
[still clean]