Monday, December 10, 2018

How to write better/safer functions for UNIX commands through the example of 'mkdir'

UNIX shell scripts contain loads of UNIX commands. Very often these commands are invoked in a simple manner which do not quite meet the standards of being production worthy.
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).
Before I start with my example I'd like to highlight an important thing which is also a main driver for this tutorial: many UNIX commands fail in a very simple manner. There can be a number of reasons for failure but the command always exits with exit code 1 and writes a more or less descriptive error message (to stderr). Not all of the failure reasons might lead to the same behaviour, reason A might be grave and imply exiting the script, reason B might be skipped.

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
    
Now assume that sometimes you want to exit if mkdir fails and sometimes not. An idea is to write a wrapper function which contains all the logic.
# 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
done
A 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 Java java.io or java.nio packages would throw a Java IOException, again not very distinguishable and too simple.

Note 2

I wonder why there is no shell library around to cover this topic, basically a shell library of UNIX command wrapper functions. Maybe I have not searched enough.

No comments:

Post a Comment