Shell scripting tutorials – more prefix commands

The previous article on shell scripting of prefix commands introduced the subject, introduced some pre-defined prefix commands and then showed how to write a basic prefix command using the sleep-before command as an example.

This article continues in the same vein, building a set of prefix commands and then showing how to combine them.

The Wait-After Command

Another useful prefix command is the wait-after command which waits for user input when a command has completed. I use this for background scripts running in command windows to stop the shell from exiting and thereby closing the window before I’ve read any output messages. Pressing return continues the script, exits the shell and closes the window.

Much of the code for this is similar to the sleep-before command, assembled in a slightly different order. Here is the usage function that also explains the options I want to make available:

function usage()
{
  cat <<-EOF
 usage: $0 [ -e ] [ -p <prompt> ] <command>
   a prefix operator that executes <command> 
   then waits for input afterwards before exiting
   <command>   : the command-line to execute in the background
   -e          : only wait on error
   -p <prompt> : the prompt printed on wait
 EOF
}

So, there are two options (apart from the -h for help, which is always implied). One is a switch that says ony wait if there was an error, otherwise exit immediately. The other is a value parameter that allows the prompt displayed on the pause to be customised.

As always, the command-line options are processed by the getopts command:

# process command-line
error=0
error_only=0
prompt="press return to finish"
while getopts p:eh option; do
  case $option in
    p)
      prompt=$OPTARG
      ;;
    e)
      error_only=1
      ;;
    h)
      usage
      exit 0
      ;;
    ?)
      error=1
      ;;
  esac
done
shift $((OPTIND-1))
# check for the existence of a script to run
if [ $# -eq 0 ]; then
  echo "$0: a command to run is required"
  error=1
fi
if [ $error -gt 0 ]; then
  usage
  exit 2
fi

Note how the switch and value parameters have been handled and the processed options are discarded by the shift after the end of the loop.

There is then a requirement that there is at least one more command-line parameter representing the command to be run, and finally a catch-all handler for errors that prints the usage and exits with the exit code 2 which is the conventional code for incorrect usage.

The next stage is to run the command itself and capture its exit status:

"$@"
status=$?

As in the previous article, the command is run by executing the remaining command-line parameters "$@" and then capturing the value of the special variable $?.

The penultimate part of the script is to now wait for user input: if the -e option is present only wait on error, otherwise always wait:

if [ $status -eq 0 ]; then
  if [ $error_only -ne 1 ]; then
    echo -n "SUCCESS: $prompt..."
    read response
  fi
else
  echo -n "ERROR: status=$status: $prompt..."
  read response
fi

The echo commands print the prompt to standard output, without a newline in this case, and then the read commands will wait for user input. All user input will be read until a return is entered, at which point the read command completes and the script continues.

The final stage is to exit with the same exit status as the command that was run before the wait:

exit $status

The Log Command

The final example of a prefix command that I will demonstrate is the log command. This is used to save the output from a command to a log file while still displaying the output on the screen.

The key to writing the log command is the pre-defined tee command. This is used to split a text stream into two streams, one to standard output and one to a file. Thus the log command is a simple script to wrap this up in a form that’s suitable for use as a prefix.

We start as usual with the shebang to say which shell to use. In this case I’m going to use the bash shell, not the usual Bourne shell (sh).

#!/bin/bash

The reason for doing this will be explained later, but basically its because we need part of the extended functionality of bash to make this work.

As usual I then write a function to print out the usage information for the script:

function usage()
{
  cat <<EOF
usage: $0 <command>
  executes the <command>, directing the output to both the>
  terminal and a log file.  This script acts as a prefix
  command - its arguments form a command to be executed.  Both
  standard output and standard error are duplicated to the
  terminal and to a log file.  The log file has the same name
  as the first argument (the command) with .log appended.  The
  log file is stored in the same directory that the command is
  run in.

  <command> : the command to execute

The command-line processing in this case is very simple, because the only option is the automatic -h option for printing help and a chack to ensure there is a command to run:

error=0
while getopts h option; do
  case $option in
    h) usage; exit 0 ;;
    ?) error=1 ;;
  esac
done
shift $((OPTIND-1))
# check for the existence of a script to run
if [ $# -eq 0 ]; then
  echo "$0: a command to run is required"
  error=1
fi
if [ $error -gt 0 ]; then
  usage
  exit 2
fi

The next stage is to generate a log file name from the first part of the suffix command to be run:

logbase=`basename $1 .exe`
logfile=${logbase}.log

This first takes the first part of the suffix command from $1 and extracts the ‘basename’ of the command, which is the name of the command with any path information stripped off. For the sake of Windows users, this optionally also strips off any .exe suffix from the command if present. So, for example, if the suffix command was ‘/usr/bin/search.exe’, then the basename would be ‘search’.

This is then converted into the name of the log output file by adding the .log extension.

The meat of the command is the running of the suffix command and the redirection into the tee command.

echo "logging output to $logfile"
"$@" 2>&1 | tee "$logfile" 
status=${PIPESTATUS[0]}

The second line is the key one. This runs the remaining command-line parameters as a sub-command ("$@"), quoting its arguments appropriately. Then, standard error is redirected to standard output (2>&1) so that both streams are now routed into a single pipe. Finally, this is piped into the tee command (| tee "$logfile") which routes the output to both standard output and to the specified logfile.

Normally, the exit status of a pipeline is the exit status of the last command, which in this case is the tee command. However, we don’t want the exit status of the tee command. The requirement for prefix commands is that they should return the exit status of the suffix command. The third line in the above snapshot (status=${PIPESTATUS[0]}) is a bash-specific feature that allows you to get the exit status of any command in a pipeline. All the exit statuses are stored in an array called PIPESTATUS, indexed from 0 upwards. In this case, we’ve extracted the first element, which is the exit status of the suffix command.

The final part of the log command is to then return this status:

exit $status

This is the basic log command. It can of course be expanded, for example to allow the log file to be redirected to a log directory, or to date/time stamp the logfile name, but these extra details are not specific to the writing of prefix commands, so I won’t go into that here.

Using Prefix Commands

Lets say I have a backup script – called backup – and I want to run it on startup. However, I don’t want it to run straight away since my internet connection might not be up and running, so I’ll delay the start for a few seconds. Also, I want the script to exit if the backup succeeded, but not if it failed. I also want to log the backup so that I can check it later. I can do this with a sequence of prefix commands which I can run as a background task on startup:

nice sleep-before -w 60 wait-after -e log backup &

So, this runs at low priority (nice). It does a sleep of 60 seconds before running the est of the command (sleep-before -w 60). It will wait after it has finished, but only on error (wait-after -e). And it will log its output to a log file (log). By putting the log command at the end of the sequence, it ensures that the log file is called ‘backup.log’, since it uses its first argument as the basename for the logfile.

Leave a Reply