I wish to have long and short forms of command line options invoked using my shell script.
I know that getopts
can be used, but like in Perl, I have not
Long options can be parsed by the standard getopts
builtin as “arguments” to the -
“option”
This is portable and native POSIX shell – no external programs or bashisms are needed.
This guide implements long options as arguments to the -
option, so --alpha
is seen by getopts
as -
with argument alpha
and --bravo=foo
is seen as -
with argument bravo=foo
. The true argument can be harvested with a simple replacement: ${OPTARG#*=}
.
In this example, -b
and -c
(and their long forms, --bravo
and --charlie
) have mandatory arguments. Arguments to long options come after equals signs, e.g. --bravo=foo
(space delimiters for long options would be hard to implement, see below).
Because this uses the getopts builtin, this solution supports usage like cmd --bravo=foo -ac FILE
(which has combined options -a
and -c
and interleaves long options with standard options) while most other answers here either struggle or fail to do that.
die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
while getopts ab:c:-: OPT; do
# support long options: https://stackoverflow.com/a/28466267/519360
if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG
OPT="${OPTARG%%=*}" # extract long option name
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=`
fi
case "$OPT" in
a | alpha ) alpha=true ;;
b | bravo ) needs_arg; bravo="$OPTARG" ;;
c | charlie ) needs_arg; charlie="$OPTARG" ;;
??* ) die "Illegal option --$OPT" ;; # bad long option
? ) exit 2 ;; # bad short option (error reported via getopts)
esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list
When the option is a dash (-
), it is a long option. getopts
will have parsed the actual long option into $OPTARG
, e.g. --bravo=foo
originally sets OPT='-'
and OPTARG='bravo=foo'
. The if
stanza sets $OPT
to the contents of $OPTARG
before the first equals sign (bravo
in our example) and then removes that from the beginning of $OPTARG
(yielding =foo
in this step, or an empty string if there is no =
). Finally, we strip the argument's leading =
. At this point, $OPT
is either a short option (one character) or a long option (2+ characters).
The case
then matches either short or long options. For short options, getopts
automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg
function, which fatally exits when $OPTARG
is empty. The ??*
condition will match any remaining long option (?
matches a single character and *
matches zero or more, so ??*
matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.
Minor bug: if somebody gives an invalid single-character long option (and it's not also a short option), this will exit with an error but without a message. This is because this implementation assumes it was a short option. You could track that with an extra variable in the long option stanza preceding the case
and then test for it in the final case condition, but I consider that too much of a corner case to bother.
(A note about all-uppercase variable names: Generally, the advice is to reserve all-uppercase variables for system use. I'm keeping $OPT
as all-uppercase to keep it in line with $OPTARG
, but this does break that convention. I think it fits because this is something the system should have done, and it should be safe because there are no standards (afaik) that use such a variable.)
To complain about unexpected arguments to long options, mimic what we did for mandatory arguments: use a helper function. Just flip the test around to complain about an argument when one isn't expected:
no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }
An older version of this answer had an attempt at accepting long options with space-delimited arguments, but it was not reliable; getopts
could prematurely terminate on the assumption that the argument was beyond its scope and manually incrementing $OPTIND
doesn't work in all shells.
This would be accomplished using one of these techniques depending on your shell:
eval "bravo=\"\$$OPTIND\""
bravo="${!OPTIND}"
bravo="${(P)OPTIND}"
and then concluded with something like [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))