Easy options parsing in shell scripts
This is an attempt to systemize my own knowledge of options parsing in shell scripts. I mainly work in Zsh, but this is compatible with Bash and possibly others.
Let’s assume that we want the CLI interface to look like this:
myscript <key> [-v <value>] [-l]
Here we have:
- required option
- two optional keys, one of which with required parameter
There’s a very handy tool – getopt
– which handles filtering and parsing command line options. Here’s how we’ll use it to define optional keys:
TEMP=`getopt lv: $@`
Here we set that we want to recognize only two options: l
without arguments and v
with one argument. Then we put whatever the script received as parameters as $@
. The getopt
command will set return status to 0
if parsing was successful and will return something else otherwise. We need to check if it’s fine and continue, or show the usage information and exit if not:
usage() {
echo "USAGE: myscript <key> [-v <value>] [-l]"
exit 1
}
if [ $? != 0 ]; then usage; fi
In case of success we move on. The result of getopt
will be saved into the TEMP
var. The way getopt
works is it puts the recognized options first, then places --
and finishes the string by listing everything it didn’t understand.
Here are some examples to illustrate:
$ getopt lv: -l
-l --
$ getopt lv: -v test
-v test --
$ getopt lv: -v test -l
-v test -l --
$ getopt lv: -v test -l xyz
-v test -l -- xyz
However, if you specify an unknown option, it will return the status code 1 and output an error like this:
$ getopt lv: -v test -a
getopt: illegal option -- a
-v test --
Let’s parse this output. First lets put the TEMP
into $@
while respecting any possible quotations.
eval set -- $TEMP
Next, let’s go through the $@
item by item and see what we encounter.
for i
do
case "$i"
in
-l)
echo "Found -l option"
shift;;
-v)
echo "Found -v option with parameter '$2'"
shift 2;;
--)
shift;
break;
esac
done
As you can see, it all is pretty easy to grasp. Two things to notice:
for i
do
done
is basically equivalent to:
for i in "$@"
do
done
And the second is that after eval set -- $TEMP
we have everything in our TEMP
var assigned to $@
and the first nine arguments to $1
through $9
. So first argument is in $1
. When we iterate over the $@
, we pick the first item, find the matching branch in case statement, act on it, then shift whatever number of arguments we consumed out of $@
and run another cycle. The exit condition is finding --
(in which case we also shift it out, in case we need the remainder).
Now that you know what this shift
command does, let’s recall that we wanted to have the required first argument. Here’s the piece we should put at the very beginning after the usage function definition.
if [ $# -lt 1 ]; then usage; fi
key=$1
shift 1
It checks if we have enough arguments, picks the first one and shifts it out in preparation to optional arguments parsing.
Here’s the full version of the script:
usage() {
echo "USAGE: myscript <key> [-v <value>] [-l]"
exit 1
}
if [ $# -lt 1 ]; then usage; fi
key=$1
shift 1
TEMP=`getopt lv: $@`
if [ $? != 0 ]; then usage; fi
setting=0
for i
do
case "$i"
in
-l)
echo "Found -l option"
shift;;
-v)
echo "Setting value of '$key' to '$2'"
setting=1
shift 2;;
--)
shift;
break;
esac
done
if [ $setting -eq 0 ]; then
echo "Reading value of '$key'"
fi
Go ahead and try it:
$ ./myscript
$ ./myscript test
$ ./myscript test -v value
$ ./myscript test -l