Skip to content

Instantly share code, notes, and snippets.

@adamhotep
Last active November 29, 2023 23:22
Show Gist options
  • Save adamhotep/895cebf290e95e613c006afbffef09d7 to your computer and use it in GitHub Desktop.
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

This is a refinement of the suboptimal code at https://stackoverflow.com/a/5255468/519360
See also my own code to support long options in POSIX shell at https://stackoverflow.com/a/28466267/519360

That second link supports arguments in the form --foo=bar while the code here supports arguments in the form --foo bar. It wouldn't be hard to adapt this code to support both while my preferred method (which only has one loop) would need to get rather ugly to do it.

@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.

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