Skip to content

Instantly share code, notes, and snippets.

@nicowilliams
Last active September 13, 2021 22:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicowilliams/f3fe2b10b380aecdef403acb246dced2 to your computer and use it in GitHub Desktop.
Save nicowilliams/f3fe2b10b380aecdef403acb246dced2 to your computer and use it in GitHub Desktop.
getopts_long() for Bash

Long and Short CLI Options getopt_long()-like for Bash

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" "$@"
}

Examples

#!/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

Note that this allows --option=argument and --option argument:

: ; 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
: ; 

Short Options Stack

: ; ./example -si foo
-si foo
foo=none id=foo silent=true; args: 0:
: ;

Optional Option Arguments

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:
: ; 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment