Shell Option Parsing in Just 15 SLOC (Excluding Blank Lines & Comments)
A while ago I posted boilerplate for portable scripts but it's rather complex. This time I wanted to see how small and simple I can make option parsing in a portable manner, using getopts, and only including the bare minimum for a functional script, while trying to stay close to the POSIX utility conventions. That means no long-options, no detailed help output with descriptions for all options but just a simple usage line, etc.
To use this, we only have to adapt the couple variables at the top to our script. Options in the options variable are required to be space separated. Options requiring a parameter are marked with a trailing ':' (colon). Only [A-Za-z0-9] are allowed as options.
The script:
#!/bin/sh
arg0=${0##*/}
options="a b: h V"
usage="[-ahV] [-b param] [operand...]"
version="getops example version 0.1"
die() { printf '%s: %s\n' "$arg0" "$*" >&2; exit 1; }
usage() { printf 'usage: %s %s\n' "$arg0" "$usage"; exit; }
version() { printf '%s\n' "$version"; exit; }
# Make sure the environment is not polluting our option variables.
for o in $options; do unset "opt_${o%:}"; done
# Parse options. We only set variables here and bail on usage errors. The
# option character prefixed by "opt_" and "arg_" will be used as variable
# names. opt_* will hold the amount of occurences (empty if 0) and arg_*
# will hold the option argument (parameter).
while getopts ":$options" o; do case $o in
:) die "option '$OPTARG' requires a parameter" ;;
\?) die "unrecognized option '$OPTARG'" ;;
*) eval "opt_$o=\$((opt_$o + 1)) arg_$o=\$OPTARG" ;;
esac; done
# Shift until only operands remain.
shift "$((OPTIND - 1))"
Besides usage errors, which are handled, the script only parses the option. To actually handle them, we could do something like this:
[ "$opt_h" ] && usage
[ "$opt_V" ] && version
[ "$opt_a" ] && printf 'Option a present %d time(s)\n' "$opt_a"
[ "$opt_b" ] && printf 'Option b parameter: %s\n' "$arg_b"
# Remaining operands.
for o do
printf '%s\n' "$o"
done
Posted Dec 26, 2021, 2:12 PM UTC
Looks okay to me, but getopts makes code unnecessarily hard to read. I would simply use a case-construct for this, as in this example: https://codeberg.org/armin/salis/src/branch/master/salis
How so? I'd argue a giant case-statement is less readable than one line and action per option:
Option parsing done like in the linked script is exactly the type I'm not a fan of, because it usually has quite a few problems:
It will exit with an error when the last option misses it's mandatory parameter, because of shifting when there are no positional parameters left (
bashdoesn't error out but most other POSIX compliant shells do). Sure, we could simply check before shifting but that means even more code, repeated for every option which requires a parameter.It does not allow combining short-options and does not handle
--to signal the end of the options (to be fair, that can easily be added but is yet more code).It doesn't properly separate option parsing and handling. After a
-h, all following usage errors are simply ignored. Sure, that's only a minor thing but most serious core utilities do it differently.So all in all it's more code for less correct behaviour.
Well I refuse to use anything but case-constructs to parse arguments these days - if you find that more readable, sure, go ahead. I also disagree with mixing upper and lower case characters in variable names ($opt_V) and with using single square bracket evaluations when writing BASH ([). [[ is much safer, you can do regular expression matching, etc. A case construct will greatly enhance the way you can parse arguments as they don't even need to be prefixed with - or --, you can just parse whatever you want, for me, personally, an absolute plus.
You didn't address any of my critique points and are really grasping at straws with yours...
My option parser dynamically sets variables using the option letters as a suffix. That's why you see something like
$opt_Vin my usage example. Got a problem with variable names mixing upper and lowercase? Simply don't use uppercase options (like -V) in your script!My script has a /bin/sh shebang and hence can't use bashisms like
[[.[Edit] s/prefix/suffix/