26 February 2007

Option-ize your shell scripts

If you're like me you may have started shell scripting very simply, maybe just to collect a number of other unwieldy commands into one executable file to call with just one short command (the name of the script). Over time (hopefully!), you will have discovered many of the other useful features of shells, like variables, redirections, and conditional expressions. As one's scripts become more complicated, one mistake a person might make is to copy and then modify a script to do something that's really only a little bit different. A better way to handle this may be to keep it all in one script and just use command line options to that script. Once you start using options to your scripts (as opposed to just arguments), all sorts of great possibilities open up.

I'm using the bash shell here and this probably won't work the same way with most other shells. Below, I've put an options demo' script. If you want to give it a try, you should have bash installed and then just paste the below into a file and 'chmod' it to be executable. This system of using options with your scripts will allow boolean options, defaults with option overrides, options with arguments, options in a blob (e.g., -aBbdg5) or singly, some crude syntax checking of the options, and required arguments as well. So, first the script, then some example output, and finally some discussion of the nuts and bolts for the curious.
#!/usr/bin/env bash
USAGE='usage: '`basename $0`' [-13ds] [-o out-file] filename'
snmpver=2c
sync=0
debug=0
while [[ ${1:0:1} = '-' ]] ; do
N=1
L=${#1}
while [[ $N -lt $L ]] ; do
case ${1:$N:1} in
'd') debug=1 ;;
's') sync=1 ;;
'1') snmpver=1 ;;
'3') snmpver=3 ;;
'o') if [[ $N -ne $(($L-1)) || ! -n ${2} ]] ; then
echo $USAGE
exit 1
fi
outfile=${2}
shift ;;
*) echo $USAGE
exit 1 ;;
esac
N=$(($N+1))
done
shift
done
if [[ ! -n ${1} ]] ; then
echo $USAGE
exit 1
fi
infile=$1
echo -n "snmpver:$snmpver debug:$debug sync:$sync "
echo "outfile:$outfile infile:$infile"
I'd normally put more white space in a script, but I want to keep it tight here. Some example output with short comments:
$ optionize.bsh
usage: optionize.bsh [-13ds] [-o ] filename
$ #that last argument _is_ required
$
$ optionize.bsh blah
snmpver:2c debug:0 sync:0 outfile: infile:blah
$ #defaults
$
$ optionize.bsh -s1 -o yada blah
snmpver:1 debug:0 sync:1 outfile:yada infile:blah
$ #blobs are fine
$
$ optionize.bsh -sd1 -o yada -3 blah
snmpver:3 debug:1 sync:1 outfile:yada infile:blah
$ #last option on the command line overrides any previous
$
$ optionize.bsh -s -d -d1o yada -3o Yadaya blah
snmpver:3 debug:1 sync:1 outfile:Yadaya infile:blah
$ #options requiring arg.s can be in a blob, but ...
$
$ optionize.bsh -d -so1 yada blah
usage: optionize.bsh [-13ds] [-o ] filename
$ # ... not in the middle of a blob, obviously.
How this works is really pretty simple if you're familiar with bash's positional parameters and shell parameter expansions. For a script called from the command line like this, the positional parameters will be each of the arguments numbered from left to right, from 1 to the number of arguments. In my first while loop, each time I finished processing an option I called the shell command 'shift', which pops off that first positional parameter and shifts the remaining ones down one position each. In the case of the '-o' option I first checked that it wasn't stuck in the middle of an options blob, then I took the following positional parameter ("$2") as the value for the option "outfile" and performed an extra shift. BTW, I was thinking here that if "outfile" was not specified that we'd just send the output to standard out, but you could just as easily have specified a default out file.

The other confusing part might be the shell parameter expansions. Briefly, "${1:0:1}" is using bash substring expansion and the syntax is "${parameter:offset:length}". So, it's short for the first character of positional parameter 1 (yes, it is zero-indexed). Similarly, "${1:$N:1}" is taking one character at a time from an option blob. And, "${#1}" is short for the number of characters in positional parameter 1.

(Can you see which variable in the script wasn't necessary? :) )

If you want to learn more about these parameter expansions, or bash in general, look here: Shell Parameter Expansion.

Labels:

1 Comments:

At 18/9/07 08:40, Anonymous Michael said...

Thanks for the nice post!

 

Post a Comment

<< Home