I don't want to discuss here what standards of production worthiness are but I want to show a few steps to improve your scripts from using a very simple command invocation to a more refined usage of the command.
My main consideration is this:
- easiness of trouble shooting: scripts will fail. The script should behave in a way to make it easy for a troubleshooter to identify the root cause of the failure.
This implies: errors should be handled properly i.e. they should be
- caught,
- documented e.g. in log files and
- finished. Finishing means e.g. revoking certain actions being done beforehand, cleaning up temporary stuff, continuing with other actions or exiting the script (if appropriate).
I'll explain this through the example of
mkdir
. As a teaser here is a common scenario:scripts often create temporary directories at the start. If the temporary directory already exists you probably don't care and despite of a mkdir failure would like to continue. If mkdir fails because the script is executed in a wrong directory where you don't have write permissions you probably don't want to continue and rather exit. This should be handled in your code.
Here is the starting point: a simple invocation of mkdir
.
mkdir tmpdir
If this command fails the script will show an error message and probably exit e.g. if set -e
is set in bash.
If you want to catch the error and decide what to do there are some options. You could use the shell binary operators AND (&&) or OR (||).
- you definitely want to exit:
mkdir tmpdir || exit 1
- you do not want to exit the script but write a specific message:
mkdir tmpdir || echo "WARNING - mkdir of tmpdir failed"
- you want to define some actions (which might include an exit):
mkdir tmpdir if [ $? -ne 0 ] ; then # Do something here e.g. echo "ERROR - mkdir of tmpdir failed. Exiting." >&2 exit 1 fi
# Function 'mkDir' # $1: Y/N flag to exit (Y) or not (N) in case of failure # $2: the directory to be created function mkDir() { mkdir "$2" # execute mkdir command rc=$? # capture return code # If successful return immediately [ $rc -eq 0 ] && return 0 # Check if exit is required if [ "$1" = "Y" -o "$1" = "y" ] ; then echo "ERROR - mkdir failed for $2" >&2 exit 1 fi echo "WARNING - mkdir failed for $2" >&2 return $rc } # invocation mkDir Y tmpdir # A loop to create a number of directories. # We want to loop through all directories # regardless if a mkdir fails or not and # create a tmpfile in each of them. for tmpdir in a b c ; do mkDir n $tmpdir || continue touch $tmpdir/tmpfile doneA trickier handling would be to distinguish different errors. Two main reasons for a mkdir failure are
- the directory already exists
- the directory cannot be created because of issues with parent directory of the new directory (which could be . or an absolute path) like missing write permissions (w missing for owner, group or other, depending on who runs the script where)
So here is an extended version of our wrapper function, also adding different return codes for various findings.
# Function 'mkDir' # $1: Y/N flag to exit (Y) or not (N) in case of failure # $2: the directory to be created # Return codes: # 0: all ok, directory could be created # 1: mkdir failed (for a different reason than the assertions) # 2: directory already exists function mkDir() { DIR="$2" # Before executing mkdir we run a couple of assertions. # Check if directory already exists [ -d "$DIR" ] && echo "WARNING - directory $DIR already exists" && return 2 # Check if parent directory exists and is writable # If not: we exit here PARENTDIR=`dirname $DIR` [ ! -d "$PARENTDIR" ] && echo "ERROR - parent directory $PARENTDIR does not exist" && exit 1 [ ! -w "$PARENTDIR" ] && echo "ERROR - parent directory $PARENTDIR is not writable" && exit 1 # Now we try to run the mkdir command mkdir "$DIR" rc=$? # capture return code # If successful return immediately [ $rc -eq 0 ] && return 0 # Here we know: mkdir failed but not for the two reasons we checked earlier # Check if exit is required if [ "$1" = "Y" -o "$1" = "y" ] ; then echo "ERROR - mkdir of $DIR failed" >&2 exit 1 fi echo "WARNING - mkdir of $DIR failed" >&2 return $rc } # Invocations mkDir n tmpdir mkDir n tmpdir # this one fails because tmpdir already exists mkDir n abc/tmpdir # this one fails if abc does not exist mkDir n /tmpdir # this one fails because we cannot write to a root owned directory
The examples above were mainly showing how to capture and finish failures. I probably will write about documenting errors in a separate blog which will cover separation or unification of stderr and stdout, redirecting messages to a proper log file and if needed show messages in parallel on the terminal.
Conclusion
In order to make life easier when trouble shooting script failures it it advisable to wrap UNIX commands into functions and create some error handling logic for known and common error causes. In the long run and in particular if used by a number of different persons you will save time since the script will throw out a specific defined error message. The various error causes of a command can drive different behaviour in the script which does help write better logic flows.Note 1
Using other programming languages does not necessarily help. The Javajava.io
or java.nio
packages would throw a Java IOException
, again not very distinguishable and too simple.