Ever wanted to write bash scripts that can take long option names, but then struggled to have anything like a normal Unix program's handling of long and short options?
Yeah, me too. Finally I wrote a getopts_long()
for Bash:
# getopts_long long_opts_assoc_array_name optstring optname args...
#
# long_opts_assoc_array_name is the name of an associative array whose
# indices are long option names and whose values are either the empty
# string (option takes no argument) or : (option takes an argument).
#
# optstring is an optstring value for getopts
#
# optname is the name of a variable in which to put the matched option
# name / letter.
#
# args... is the arguments to parse.
#
# As with getopts, $OPTIND is set to the next argument to check at the
# next invocation. Unset OPTIND or set it to 1 to reset options
# processing.
#
# As with getopts, "--" is a special argument that ends options
# processing.
#
function getopts_long {
if (($# < 3)); then
printf 'bash: illegal use of getopts_long\n'
printf 'Usage: getopts_long lvar optstring name [ARGS]\n'
printf '\t{lvar} is the name of an associative array variable\n'
printf '\twhose keys are long option names and values are\n'
printf '\tthe empty string (no argument) or ":" (argument\n'
printf '\trequired).\n\n'
printf '\t{optstring} and {name} are as for the {getopts}\n'
printf '\tbash builtin.\n'
return 1
fi 1>&2
[[ ${1:-} != largs ]] && local -n largs="$1"
local optstr="$2"
[[ ${3:-} != opt ]] && local -n opt="$3"
local optvar="$3"
shift 3
OPTARG=
: "${OPTIND:=1}"
opt=${@:$OPTIND:1}
if [[ $opt = -- ]]; then
opt='?'
return 1
fi
if [[ $opt = --* ]]; then
local optval=false
opt=${opt#--}
if [[ $opt = *=* ]]; then
OPTARG=${opt#*=}
opt=${opt%%=*}
optval=true
fi
((++OPTIND))
if [[ ${largs[$opt]+yes} != yes ]]; then
((OPTERR)) && printf 'bash: illegal long option %s\n' "$opt" 1>&2
return 0
fi
if [[ ${largs[$opt]:-} = : ]]; then
if ! $optval; then
OPTARG=${@:$OPTIND:1}
((++OPTIND))
fi
fi
return 0
fi
getopts "$optstr" "$optvar" "$@"
}
#!/bin/bash
. getopts_long
declare -A long=([foo]=: [id]=: [silent]='')
foo=none
id=$USER
silent=false
while getopts_long long f:i:sx opt "$@"; do
case "$opt" in
foo|f) foo=$OPTARG;;
id|i) id=$OPTARG;;
silent|s) silent=true; [[ -n $OPTARG ]] && echo "Look ma: optional option arguments! --silent=$OPTARG";;
x) set -vx;;
*) echo "Usage: $0 [--foo FOO | -f FOO] [--id USER | -i USER] [--silent | -s] [-x] ARGS" 1>&2; exit 1;;
esac
done
shift $((OPTIND-1))
echo "foo=$foo id=$id silent=$silent; args: $#: $*"
exit 0
: ; printf 'PS1="%s"\n' "$PS1" # makes it easy to copy-paste
PS1=": ;"
: ;
: ; ./example --foo=bar --silent --id baz abc xyz
foo=bar id=baz silent=true; args: 2: abc xyz
: ; ./example --foo bar --silent --id foo abc xyz
foo=bar id=foo silent=true; args: 2: abc xyz
: ;
Errors are handled as one would expect:
: ; ./example --baz=bar --silent --id foo abc xyz
bash: illegal long option baz
: ; echo $?
1
: ;
: ; ./example -si foo
-si foo
foo=none id=foo silent=true; args: 0:
: ;
A monstrosity that is allowed: long options declared as not having an argument are allowed to have one when given as --option-name=optional-argument
:
: ; ./example --silent=heh -i foo
Look ma: optional option arguments! --silent=heh
foo=none id=foo silent=true; args: 0:
: ;