2007-10-05

Shell Scripting: friendly command-line arguments

Ah, the joys of shell scripting! If you've spent any time on UNIX-like operating systems, you've probably encountered or written shell scripts. The theory is simple. For the most part, shell scripts simply execute shell commands in order. You find them everywhere. Simply booting a Linux or BSD host might execute scores of shell scripts. Scheduled processes like those launched with cron or at are usually shell scripts. As a system administrator or a hacker, well-programmed scripts can make your life and the lives of your users a lot easier. On the other hand, scripts that are arcane and cryptic can be more trouble than they're worth.

One of the major hang-ups of complex shell scripts is the strict syntax of the command-line arguments. When calling the command-line arguments from within the script, the first argument is referenced as $1, the next as $2 and so on. $0 is the name of the script itself as it was entered on the command line (including the path, if typed). Also, $# is a numeric variable that contains the number of command line arguments passed. Using the exit command is a way to make sure the script stops where it is without processing any further commands. Using exit 0 creates a "clean" exit, whereas exit 1 (or any other integer) is a way to symbolize an error. This doesn't matter much unless other scripts rely on the ones you're making. It's good practice to specify a proper exit status for your scripts, but it's not mandatory.

Most shell scripts that accept arguments require the end-user to know exactly what arguments to pass or they will simply fail. Take, for example, this script I wrote to get my wireless adapter online in OpenBSD.

-------------------------------------------------------------------------------


#!/bin/sh
sudo ifconfig $1 nwid $2 nwkey $3 up
sudo dhclient $1

-------------------------------------------------------------------------------

It requires me to select the device name of my wireless ethernet adapter, the SSID, and the WEP password. The command line could look like this:

wifi.sh ural0 mywlan 0x31337e1ee7

If I just executed wifi.sh without any arguments, the ifconfig would fail miserably on syntax alone, and dhclient would not know what ethernet adapter to use to get an IP address. The script would not work.

Some more advanced scripts will determine if you entered enough arguments. If you did not, it may give you some brief explanation as to what it wants for arguments. The "apachectl" script for controlling the Apache Web Server is a good example of this. If you run it alone, you are shown a list of arguments that it accepts:

usage: apachectl [ start | startssl | stop | restart | graceful |
status | fullstatus | configtest | help ]
<... output truncated ...>

This style of script is fairly straight-forward. You code it to accept one command line argument, referenced as $1 using the case command as shown in this simple example:

-------------------------------------------------------------------------------

#!/bin/sh
if [ -z "$1" ]
then
echo "usage: $0 [ start | stop ]"
exit 1
fi
case $1 in
start)
echo "You chose start!"
;;
stop)
echo "you chose stop!"
;;
*)
echo "I'm sorry, you didn't choose stop or start."
;;
esac

-------------------------------------------------------------------------------

You can fill in the echo commands with whatever you find useful. This is fairly mundane, and doesn't allow you to pass parameters or multiple flags to your script. For those unfamiliar with the "case" command, it's quite simple to use. If the contents of the variable referenced in the "case" line match the expression before the parenthesis, it executes the code on the following lines, and stops processing when it encounters two semicolons.

Let's face it, with just a single "case" structure and some error checking, you won't be writing any truly powerful shell scripts.

Enter "shift". Within a shell script, shift destroys $1 and shifts all the other arguments down by one, and decrements the value in $# by one as well in order to reflect the new (lower) number of command-line arguments left. The contents of $2 become $1, $3 becomes $2, etc. While you might not think that sounds too exciting, it will allow you to pull off some argument-processing trickery with a simple loop to read arguments. Check out this example:

-------------------------------------------------------------------------------

#!/bin/sh
until [ $# == 0 ]
do
case $1 in
foo)
echo "you have selected foo"
shift
;;
baz)
echo "you have selected baz"
shift
;;
bar)
echo "you have selected bar"
shift
;;
zot)
echo "you have selected zot"
shift
;;
*)
#ignore arguments we don't recognize, just shift them and move on
shift
;;
esac
done

-------------------------------------------------------------------------------

The until/do loop above simply keeps running the arguments through the case block until there are 0 arguments left, then exits. If you pass it an argument that is not in the case block, it simply does a shift and ignores it. It runs the arguments in the order we choose.

bash-3.1$ ./foo.sh foo bar
you have selected foo
you have selected bar

In the case of my made-up wireless configuration script, this isn't directly all that helpful. Another thing you can do, however, is run another shift within a case. This allows you to give your script many very flexible command flags, much like other UNIX commands. Using my wifi script as an example, I'll show you how it's done. For arguments that don't require a second parameter (such as -h and -d) just use one shift statement within the case. For arguments that do require a parameter (such as -d, -s, -k, or -p, use two shifts: one before you assign $1 to a variable, and again at the end of the case. Notice that the * catch-all case is simply there to shift un-recognized arguments. If we don't do this, our loop will hang forever because there will always be an argument that hasn't been processed.

This script puts it all together. Its arguments are just as flexible as most compiled programs. After the script has processed all of the arguments, I use a series of if statements to build the command line for ifconfig by appending to the $ifconfig_args variable, then run dhclient if desired.

There's a little extra scripting (more if statements) to make sure that a value follows the arguments that require another parameter. In the end, this is a pretty lengthy script, but it's almost bullet-proof and a lot friendlier than most shell scripts. People might not even know it's a script!

-------------------------------------------------------------------------------

#!/bin/sh
if [ -z $1 ]
then
echo "Try using '$0 -h' for help."
exit 1
fi
until [ $# == 0 ]
do
case $1 in
-h)
help=1
shift
;;
-d)
shift
if [ -z "$1" ]
then
echo "You must specify a device with -d"
exit 2
fi
device=$1
shift
;;
-s)
shift
if [ -z "$1" ]
then
echo "You must specify an SSID with -s"
exit 2
fi
ssid=$1
shift
;;
-p)
shift
if [ -z "$1" ]
then
echo "You must specify a password with -p"
exit 2
fi
password=$1
shift
;;
-k)
shift
if [ -z "$1" ]
then
echo "You must specify a key with -k"
exit 2
fi
wepkey=$1
shift
;;
-c)
dhcp=1
shift
;;
*)
shift
;;
esac
done

if [ "$help" ]
then
echo "Usage: $0 -d -s [-h] [-c] [-p | -k ]"
echo "-d Wireless Ethernet device (wi0, ural0, etc.)"
echo "-s The SSID of the network you wish to join"
echo "-h This help page"
echo "-c Start DHCP client"
echo "-p 5 or 13 character WEP password"
echo "-k 10 or 26 character hexadecimal WEP key"
exit 0
fi
if [ -z "$ssid" ]
then
echo "You must specify an SSID"
exit 1
fi

if [ -z "$device" ]
then
echo "You must specify a device"
exit 1
fi

ifconfig_args="$device nwid $ssid"
if [ "$password" ]
then
ifconfig_args="$ifconfig_args nwkey $password"
fi

if [ "$wepkey" ]
then
ifconfig_args="$ifconfig_args nwkey 0x$wepkey"
fi

ifconfig $ifconfig_args

if [ "$dhcp" ]
then
dhclient $device
fi

exit 0

-------------------------------------------------------------------------------

The ifconfig syntax I used in my examples is fairly platform specific to the BSD family, but you can change it to work on Linux, Solaris, or any other UNIX-like OS.

The UNIX userland contains hundreds of little utilities that can be strung together with scripts and pipes to create very powerful programs without having to spend a lot of time learning a new programming language. Hopefully you don't just learn how to make an ifconfig script out of this, but take what I've written as an example of how to improve your own scripts or inspire you to start creating your own scripts.

blog comments powered by Disqus