Skip to content

Instantly share code, notes, and snippets.

@adamhotep
Last active February 26, 2026 14:25
Show Gist options
  • Select an option

  • Save adamhotep/895cebf290e95e613c006afbffef09d7 to your computer and use it in GitHub Desktop.

Select an option

Save adamhotep/895cebf290e95e613c006afbffef09d7 to your computer and use it in GitHub Desktop.
POSIX shell: support long options by converting them to short options
# a refinement of https://stackoverflow.com/a/5255468/519360
# see also my non-translating version at https://stackoverflow.com/a/28466267/519360
# translate long options to short
reset=true stopped=""
for opt in "$@"; do
if [ -n "$reset" ]; then
unset reset
set -- # reset the "$@" array so we can rebuild it
fi
case "$opt" in # --option=argument -> opt='--option' optarg='argument'
--?*=* ) optarg="${opt#*=}" opt="${opt%%=*}" ;;
* ) unset optarg ;;
esac
case "$stopped$opt" in
-- ) stopped=true; set -- "$@" -- ;;
--help ) set -- "$@" -h ;;
--verbose ) set -- "$@" -v ;;
--config ) set -- "$@" -c ${optarg+"$optarg"} ;;
--long-only ) DEMO_LONG_ONLY_FLAG=true ;;
# pass anything else through, including spaced arguments
* ) set -- "$@" "$opt" ;;
esac
done
# now we can process with getopt
while getopts ":hvc:" opt; do
case $opt in
h ) usage ;;
v ) VERBOSE=true ;;
c ) source $OPTARG ;;
\? ) usage ;;
: )
echo "option -$OPTARG requires an argument"
usage
;;
esac
done
shift $((OPTIND-1))
@adamhotep
Copy link
Author

Today's edits add support for --option=argument without as much ugliness as previously anticipated. If you don't want that, remove the first case stanza and the ${optarg:+"$optarg"} part of --config (though leaving them in is harmless).

This code uses a some parameter substitutions. The first one, ${opt#*=}, takes the value of $opt without the first = and the non-equals-sign characters that precede it (aka s/^[^=]*=//). The second one, ${opt%%=*}, pulls greedily from the end, removing the first = and everything that follows it (aka s/=.*$//).

The third subsitution, ${optarg+"$optarg"}, ensures we only add the argument when it was actually defined. If we used "$optarg" instead, we'd be adding an empty string as the argument and --config foo.conf would become -c '' foo.conf which will run source '' (resulting in sh: 31: source: not found) and getopts will terminate given the standalone foo.conf even if more options follow.

This is a little tricky. If we used ${optarg:+"$optarg"} instead, that extra colon changes the logic given an empty assignment. Consider:

unset test # $test is not defined
set -- a ${test+"$test"}
echo $#    # there is ONE parameter
set -- a ${test:+"$test"}
echo $#    # there is ONE parameter

test=""    # $test is defined but empty
set -- a ${test+"$test"}
echo $#    # there are TWO parameters
set -- a ${test:+"$test"}
echo $#    # there is ONE parameter

@adamhotep
Copy link
Author

Aside from needing a preprocessing loop, this approach has a flaw in that it loses the long option name; if you trigger that : clause (meaning you've forgotten an option's argument), the complaint uses $opt (which getopts has converted to $OPTARG), e.g. -c in place of --config.

Working around that is only a little ugly: Add "--$opt" after each set -- "$@" in the second case of the for loop excluding the * clause. Before that final clause, add a new -* ) set -- "$@" "--$opt" "$opt" ;; clause. Add -: to the getopts optstring. Add - ) param="$OPTARG" ;; to the getopts loop's case stanza, and then refer to $param instead of -$OPTARG.

@jbrubake
Copy link

If you pass a long-only option that requires an argument, the above code only works if you pass it like --option=arg. It does not work without the =. I got it to work like this but is there a better way:

    reset=true stopped="" grab=
    for option in "$@"; do
    ...    
        # If $grab is set then we have an option waiting for an argument
        if [ -n "$grab" ]; then
            optarg=$opt # $opt holds the needed argument
            opt=$grab   # recheck the previous option
            unset grab  # unset the flag
        fi

        case "$stopped$opt" in
           --long-only)
                # $optarg is normally only set if we passed --opt=arg. If we passed
                # '--opt arg' without the '=' we need to grab the next value of $opt
                # as our argument
                #
                # $optarg is set so we can process the option
                if [ -n "$optarg" ]; then
                    echo "Got --long-only=$optarg"
                # $optarg is not set so we set a flag to the name of this option so
                # the next time through the loop we can match this block again
                else
                    grab=--long-only
                fi
                ;;
    ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment