Skip to content

Instantly share code, notes, and snippets.

@wwerner
Created November 6, 2018 10:48
Show Gist options
  • Save wwerner/31758b8083746eb285e7eb62b55e532e to your computer and use it in GitHub Desktop.
Save wwerner/31758b8083746eb285e7eb62b55e532e to your computer and use it in GitHub Desktop.
#! /bin/echo Usage:.
#
# getopts_long -- POSIX shell getopts with GNU-style long option support
#
# Copyright 2005-2009 Stephane Chazelas <stephane_chazelas@yahoo.fr>
#
# Permission to use, copy, modify, distribute, and sell this software and
# its documentation for any purpose is hereby granted without fee, provided
# that the above copyright notice appear in all copies and that both that
# copyright notice and this permission notice appear in supporting
# documentation. No representations are made about the suitability of this
# software for any purpose. It is provided "as is" without express or
# implied warranty.
# History:
# 2005 ?? - 1.0
# first version
# 2009-08-12 - 1.1
# thanks to ujqm8360@netwtc.net for helping fix a few bugs:
# - -ab x where -b accepts arguments wasn't handled properly. Also,
# $OPTLIND wasn't set properly (at least not the same way as most
# getopts implementation do).
# - The handling of ambiguous long options was incorrect.
getopts_long() {
# args: shortopts, var, [name, type]*, "", "$@"
#
# getopts_long parses command line arguments. It works like the
# getopts shell built-in command except that it also recognises long
# options a la GNU.
#
# You must provide getopts_long with the list of supported single
# letter options in the same format as getopts', followed by the
# variable name you want getopts_long to return the current processed
# option in, followed by the list of long option names (without the
# leading "--") and types (0 or no_argument, 1 or required_argument,
# 2 or optional_argument). The end of the long option specification
# is indicated by an empty argument. Then follows the list of
# arguments to be parsed.
#
# The $OPTLIND variable must be set to 1 before the first call of the
# getopts_long function to process a given list of arguments.
#
# getopts_long returns the value of the current option in the variable
# whose name is provided as its second argument (be careful to avoid
# variables that have a special signification to getopts_long or the
# shell or any other tool you may call from your script). If the
# current option is a single letter option, then it is returned
# without the leading "-". If it's a long option (possibly
# abbreviated), then the full name of the option (without the leading
# "--") is returned. If the option has an argument, then it is stored
# in the $OPTLARG variable. If the current option is not recognised,
# or if it is provided with an argument while it is not expecting one
# (as in --opt=value) or if it is not provided with an argument while
# it is expecting one, or if the option is so abbreviated that it is
# impossible to identify the option uniquely, then:
# - if the short option specifications begin with ":", getopts_long
# returns ":" in the output variable and $OPTLARG contains the
# faulty option name (in full except in the case of the ambiguous
# or bad option) and $OPTLERR contains the error message.
# - if not, then getopts_long behaves the same as above except that
# it returns "?" instead of ":", leaves $OPTLARG unset and
# displays the error message on stderr.
#
# The exit status of getopts_long is 0 unless the end of options is
# reached or an error is encountered in the syntax of the getopts_long
# call.
#
# After getopts_long has finished processing the options, $OPTLIND
# contains the index of the first non-option argument or $# + 1 if
# there's no non-option argument.
#
# The "=" character is not allowed in a long option name. Any other
# character is. "-" and ":" are not allowed as short option names. Any
# other character is. If a short option appears more than once in the
# specification, the one with the greatest number of ":"s following it
# is retained. If a long option name is provided more than once, only
# the first one is taken into account. Note that if you have both a -a
# and --a option, there's no way to differentiate them. Beside the
# $OPTLIND, $OPTLARG, and $OPTLERR, getopts_long uses the $OPTLPENDING
# variable to hold the remaining options to be processed for arguments
# with several one-letter options. That variable shouldn't be used
# anywhere else in your script. Those 4 variables are the only ones
# getopts_long may modify.
#
# Dependency: only POSIX utilities are called by that function. They
# are "set", "unset", "shift", "break", "return", "eval", "command",
# ":", "printf" and "[". Those are generally built in the POSIX
# shells. Only "printf" has been known not to be in some old versions
# of bash, zsh or ash based shells.
#
# Differences with the POSIX getopts:
# - if an error is detected during the parsing of command line
# arguments, the error message is stored in the $OPTLERR variable
# and if the first character of optstring is ':', ':' is returned in
# any case.
# - in the single-letter option specification, if a letter is
# followed by 2 colons ("::"), then the option can have an optional
# argument as in GNU getopt(3). In that case, the argument must
# directly follow the option as in -oarg (not -o arg).
# - there must be an empty argument to mark the end of the option
# specification.
# - long options starting with "--" are supported.
#
# Differences with GNU getopt_long(3):
# - getopts_long doesn't allow options to be interspersed with other
# arguments (as if POSIXLY_CORRECT was set for GNU getopt_long(3))
# - there's no linkage of any sort between the short and long
# options. The caller is responsible of that (see example below).
#
# Compatibility:
# getopts_long code is (hopefully) POSIX.2/SUSv3 compliant. It won't
# work with the Bourne/SystemV shell. Use /usr/xpg4/bin/sh or ksh or
# bash on Solaris.
# It has been tested successfully with:
# - bash 3.0 (patch level 16) on Cygwin
# - zsh 4.2.4 on Solaris 2.7
# - /usr/xpg4/bin/sh (same as /usr/bin/ksh) (ksh88i) on Solaris 2.7
# - /usr/dt/bin/dtksh (ksh93d) on Solaris 2.7
# - /usr/bin/ksh (pdksh 5.2.14) on Linux
# - zsh 3.0.6 on Solaris 2.8
# - bash 2.0.3 on Solaris 2.8
# - dash 0.5.2 on Linux
# - bash 2.05b (patch level 0) on Linux
# - ksh93p and ksh93q on Linux (ksh93t+ crashes)
#
# It is known to fail with those non-POSIX compliant shells:
# - /bin/sh on Solaris
# - /usr/bin/sh on Cygwin
# - bash 1.x
#
# Bugs:
# please report them to <stephane_chazelas@yahoo.fr>
#
# Example:
#
# verbose=false opt_bar=false bar=default_bar foo=default_foo
# opt_s=false opt_long=false
# OPTLIND=1
# while getopts_long :sf:b::vh opt \
# long 0 \
# foo required_argument \
# bar 2 \
# verbose no_argument \
# help 0 "" "$@"
# do
# case "$opt" in
# s) opt_s=true;;
# long) opt_long=true;;
# v|verbose) verbose=true;;
# h|help) usage; exit 0;;
# f|foo) foo=$OPTLARG;;
# b|bar) bar=${OPTLARG-$bar};;
# :) printf >&2 '%s: %s\n' "${0##*/}" "$OPTLERR"
# usage
# exit 1;;
# esac
# done
# shift "$(($OPTLIND - 1))"
# # process the remaining arguments
[ -n "${ZSH_VERSION+z}" ] && emulate -L sh
unset OPTLERR OPTLARG || :
case "$OPTLIND" in
"" | 0 | *[!0-9]*)
# First time in the loop. Initialise the parameters.
OPTLIND=1
OPTLPENDING=
;;
esac
if [ "$#" -lt 2 ]; then
printf >&2 'getopts_long: not enough arguments\n'
return 1
fi
# validate variable name. Need to fix locale for character ranges.
LC_ALL=C command eval '
case "$2" in
*[!a-zA-Z_0-9]*|""|[0-9]*)
printf >&2 "getopts_long: invalid variable name: \`%s'\''\n" "$2"
return 1
;;
esac'
# validate short option specification
case "$1" in
::*|*:::*|*-*)
printf >&2 "getopts_long: invalid option specification: \`%s'\n" "$1"
return 1
;;
esac
# validate long option specifications
# POSIX shells only have $1, $2... as local variables, hence the
# extensive use of "set" in that function.
set 4 "$@"
while :; do
if
[ "$1" -gt "$#" ] || {
eval 'set -- "${'"$1"'}" "$@"'
[ -n "$1" ] || break
[ "$(($2 + 2))" -gt "$#" ]
}
then
printf >&2 "getopts_long: long option specifications must end in an empty argument\n"
return 1
fi
eval 'set -- "${'"$(($2 + 2))"'}" "$@"'
# $1 = type, $2 = name, $3 = $@
case "$2" in
*=*)
printf >&2 "getopts_long: invalid long option name: \`%s'\n" "$2"
return 1
;;
esac
case "$1" in
0 | no_argument) ;;
1 | required_argument) ;;
2 | optional_argument) ;;
*)
printf >&2 "getopts_long: invalid long option type: \`%s'\n" "$1"
return 1
;;
esac
eval "shift 3; set $(($3 + 2))"' "$@"'
done
shift
eval "shift; set $(($1 + $OPTLIND))"' "$@"'
# unless there are pending short options to be processed (in
# $OPTLPENDING), the current option is now in ${$1}
if [ -z "$OPTLPENDING" ]; then
[ "$1" -le "$#" ] || return 1
eval 'set -- "${'"$1"'}" "$@"'
case "$1" in
--)
OPTLIND=$(($OPTLIND + 1))
return 1
;;
--*)
OPTLIND=$(($OPTLIND + 1))
;;
-?*)
OPTLPENDING="${1#-}"
shift
;;
*)
return 1
;;
esac
fi
if [ -n "$OPTLPENDING" ]; then
# WA for zsh and bash 2.03 bugs:
OPTLARG=${OPTLPENDING%"${OPTLPENDING#?}"}
set -- "$OPTLARG" "$@"
OPTLPENDING="${OPTLPENDING#?}"
unset OPTLARG
# $1 = current option = ${$2+1}, $3 = $@
[ -n "$OPTLPENDING" ] ||
OPTLIND=$(($OPTLIND + 1))
case "$1" in
[-:])
OPTLERR="bad option: \`-$1'"
case "$3" in
:*)
eval "$4=:"
OPTLARG="$1"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$4='?'"
;;
esac
;;
*)
case "$3" in
*"$1"::*) # optional argument
eval "$4=\"\$1\""
if [ -n "$OPTLPENDING" ]; then
# take the argument from $OPTLPENDING if any
OPTLARG="$OPTLPENDING"
OPTLPENDING=
OPTLIND=$(($OPTLIND + 1))
fi
;;
*"$1":*) # required argument
if [ -n "$OPTLPENDING" ]; then
# take the argument from $OPTLPENDING if any
OPTLARG="$OPTLPENDING"
eval "$4=\"\$1\""
OPTLPENDING=
OPTLIND=$(($OPTLIND + 1))
else
# take the argument from the next argument
if [ "$(($2 + 2))" -gt "$#" ]; then
OPTLERR="option \`-$1' requires an argument"
case "$3" in
:*)
eval "$4=:"
OPTLARG="$1"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$4='?'"
;;
esac
else
OPTLIND=$(($OPTLIND + 1))
eval "OPTLARG=\"\${$(($2 + 2))}\""
eval "$4=\"\$1\""
fi
fi
;;
*"$1"*) # no argument
eval "$4=\"\$1\""
;;
*)
OPTLERR="bad option: \`-$1'"
case "$3" in
:*)
eval "$4=:"
OPTLARG="$1"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$4='?'"
;;
esac
;;
esac
;;
esac
else # long option
# remove the leading "--"
OPTLPENDING="$1"
shift
set 6 "${OPTLPENDING#--}" "$@"
OPTLPENDING=
while
eval 'set -- "${'"$1"'}" "$@"'
[ -n "$1" ]
do
# $1 = option name = ${$2+1}, $3 => given option = ${$4+3}, $5 = $@
case "${3%%=*}" in
"$1")
OPTLPENDING=EXACT
break;;
esac
# try to see if the current option can be seen as an abbreviation.
case "$1" in
"${3%%=*}"*)
if [ -n "$OPTLPENDING" ]; then
[ "$OPTLPENDING" = AMBIGUOUS ] || eval '[ "${'"$(($OPTLPENDING + 1))"'}" = "$1" ]' ||
OPTLPENDING=AMBIGUOUS
# there was another different option matching the current
# option. The eval thing is in case one option is provided
# twice in the specifications which is OK as per the
# documentation above
else
OPTLPENDING="$2"
fi
;;
esac
eval "shift 2; set $(($2 + 2)) "'"$@"'
done
case "$OPTLPENDING" in
AMBIGUOUS)
OPTLERR="option \`--${3%%=*}' is ambiguous"
case "$5" in
:*)
eval "$6=:"
OPTLARG="${3%%=*}"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$6='?'"
;;
esac
OPTLPENDING=
return 0
;;
EXACT)
eval 'set "${'"$(($2 + 2))"'}" "$@"'
;;
"")
OPTLERR="bad option: \`--${3%%=*}'"
case "$5" in
:*)
eval "$6=:"
OPTLARG="${3%%=*}"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$6='?'"
;;
esac
OPTLPENDING=
return 0
;;
*)
# we've got an abbreviated long option.
shift
eval 'set "${'"$(($OPTLPENDING + 1))"'}" "${'"$OPTLPENDING"'}" "$@"'
;;
esac
OPTLPENDING=
# $1 = option type, $2 = option name, $3 unused,
# $4 = given option = ${$5+4}, $6 = $@
case "$4" in
*=*)
case "$1" in
1 | required_argument | 2 | optional_argument)
eval "$7=\"\$2\""
OPTLARG="${4#*=}"
;;
*)
OPTLERR="option \`--$2' doesn't allow an argument"
case "$6" in
:*)
eval "$7=:"
OPTLARG="$2"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$7='?'"
;;
esac
;;
esac
;;
*)
case "$1" in
1 | required_argument)
if [ "$(($5 + 5))" -gt "$#" ]; then
OPTLERR="option \`--$2' requires an argument"
case "$6" in
:*)
eval "$7=:"
OPTLARG="$2"
;;
*)
printf >&2 '%s\n' "$OPTLERR"
eval "$7='?'"
;;
esac
else
OPTLIND=$(($OPTLIND + 1))
eval "OPTLARG=\"\${$(($5 + 5))}\""
eval "$7=\"\$2\""
fi
;;
*)
# optional argument (but obviously not provided) or no
# argument
eval "$7=\"\$2\""
;;
esac
;;
esac
fi
return 0
}
# testing code
if [ -n "$test_getopts_long" ]; then
test_getopts_long() {
expected="$1" had=
shift
OPTLIND=1
while err="$(set +x;getopts_long "$@" 2>&1 > /dev/null)"
getopts_long "$@" 2> /dev/null; do
eval "opt=\"\$$2\""
had="$had|$opt@${OPTLARG-unset}@${OPTLIND-unset}@${OPTLERR-unset}@$err"
done
had="$had|${OPTLIND-unset}|$err"
if [ "$had" = "$expected" ]; then
echo PASS
else
echo FAIL
printf 'Expected: %s\n Got: %s\n' "$expected" "$had"
fi
}
while IFS= read -r c && IFS= read -r e; do
printf '+ %-72s ' "$c"
#set -x
eval "test_getopts_long \"\$e\" $c"
done << \EOF
: a
|1|getopts_long: long option specifications must end in an empty argument
:a opt "" -a
|a@unset@2@unset@|2|
:a opt "" -a b
|a@unset@2@unset@|2|
:a opt "" -a -a
|a@unset@2@unset@|a@unset@3@unset@|3|
:a opt "" -ab
|a@unset@1@unset@|:@b@2@bad option: `-b'@|2|
:a: opt "" -ab
|a@b@2@unset@|2|
:a: opt "" -a b
|a@b@3@unset@|3|
:a: opt "" -a -a
|a@-a@3@unset@|3|
:a: opt "" -a
|:@a@2@option `-a' requires an argument@|2|
:a:: opt "" -a
|a@unset@2@unset@|2|
:a:: opt "" -ab
|a@b@2@unset@|2|
:a:: opt "" -a b
|a@unset@2@unset@|2|
:ab: opt "" -ab c
|a@unset@1@unset@|b@c@3@unset@|3|
:a:: opt "" -a -a
|a@unset@2@unset@|a@unset@3@unset@|3|
:a:: opt "" -:a:
|:@:@1@bad option: `-:'@|a@:@2@unset@|2|
:= opt ""
|1|
:: opt ""
|1|getopts_long: invalid option specification: `::'
: opt ""
|1|
:a:a opt "" -a
|:@a@2@option `-a' requires an argument@|2|
:a::a opt "" -a
|a@unset@2@unset@|2|
:ab:c:: opt "" -abc -cba -bac
|a@unset@1@unset@|b@c@2@unset@|c@ba@3@unset@|b@ac@4@unset@|4|
: opt abc 0 "" --abc
|abc@unset@2@unset@|2|
: opt abc no_argument "" --abc
|abc@unset@2@unset@|2|
: opt abc no_argument "" --abc=foo
|:@abc@2@option `--abc' doesn't allow an argument@|2|
: opt abc no_argument "" --abc foo
|abc@unset@2@unset@|2|
: opt abc 1 "" --abc=foo
|abc@foo@2@unset@|2|
: opt abc required_argument "" --abc foo
|abc@foo@3@unset@|3|
: opt abc required_argument "" --abc=
|abc@@2@unset@|2|
: opt abc required_argument "" --abc
|:@abc@2@option `--abc' requires an argument@|2|
: opt abc 2 "" --abc
|abc@unset@2@unset@|2|
: opt abc optional_argument "" --abc=
|abc@@2@unset@|2|
: opt abc optional_argument "" --abc=foo
|abc@foo@2@unset@|2|
: opt abc optional_argument "" --abc --abc
|abc@unset@2@unset@|abc@unset@3@unset@|3|
: opt abc 0 abcd 0 "" --abc
|abc@unset@2@unset@|2|
: opt abc 0 abd 0 "" --ab
|:@ab@2@option `--ab' is ambiguous@|2|
: opt abc 0 abcd 0 "" --ab
|:@ab@2@option `--ab' is ambiguous@|2|
: opt abc 0 abc 1 "" --ab
|abc@unset@2@unset@|2|
: opt abc 0 abc 1 "" --abc
|abc@unset@2@unset@|2|
: opt abc 0 abc 1 "" --ab
|abc@unset@2@unset@|2|
: opt abc 0 acd 0 "" --ab
|abc@unset@2@unset@|2|
:abc:d:e::f:: opt ab 0 ac 1 bc 2 cd 1 cde 2 "" -abcdef -a -f -c --a --a= --b=foo -fg
|a@unset@1@unset@|b@unset@1@unset@|c@def@2@unset@|a@unset@3@unset@|f@unset@4@unset@|c@--a@6@unset@|:@a@7@option `--a' is ambiguous@|bc@foo@8@unset@|f@g@9@unset@|9|
a opt "" -a
|a@unset@2@unset@|2|
a opt "" -a b
|a@unset@2@unset@|2|
a opt "" -a -a
|a@unset@2@unset@|a@unset@3@unset@|3|
a opt "" -ab
|a@unset@1@unset@|?@unset@2@bad option: `-b'@bad option: `-b'|2|
a: opt "" -ab
|a@b@2@unset@|2|
a: opt "" -a b
|a@b@3@unset@|3|
a: opt "" -a -a
|a@-a@3@unset@|3|
a: opt "" -a
|?@unset@2@option `-a' requires an argument@option `-a' requires an argument|2|
a:: opt "" -a
|a@unset@2@unset@|2|
a:: opt "" -ab
|a@b@2@unset@|2|
a:: opt "" -a b
|a@unset@2@unset@|2|
a:: opt "" -a -a
|a@unset@2@unset@|a@unset@3@unset@|3|
a:: opt "" -:a:
|?@unset@1@bad option: `-:'@bad option: `-:'|a@:@2@unset@|2|
= opt ""
|1|
: opt ""
|1|
'' opt ""
|1|
a:a opt "" -a
|?@unset@2@option `-a' requires an argument@option `-a' requires an argument|2|
a::a opt "" -a
|a@unset@2@unset@|2|
ab:c:: opt "" -abc -cba -bac
|a@unset@1@unset@|b@c@2@unset@|c@ba@3@unset@|b@ac@4@unset@|4|
'' opt abc 0 "" --abc
|abc@unset@2@unset@|2|
'' opt abc no_argument "" --abc
|abc@unset@2@unset@|2|
'' opt abc no_argument "" --abc=foo
|?@unset@2@option `--abc' doesn't allow an argument@option `--abc' doesn't allow an argument|2|
'' opt abc no_argument "" --abc foo
|abc@unset@2@unset@|2|
'' opt abc 1 "" --abc=foo
|abc@foo@2@unset@|2|
'' opt abc required_argument "" --abc foo
|abc@foo@3@unset@|3|
'' opt abc required_argument "" --abc=
|abc@@2@unset@|2|
'' opt abc required_argument "" --abc
|?@unset@2@option `--abc' requires an argument@option `--abc' requires an argument|2|
'' opt abc 2 "" --abc
|abc@unset@2@unset@|2|
'' opt abc optional_argument "" --abc=
|abc@@2@unset@|2|
'' opt abc optional_argument "" --abc=foo
|abc@foo@2@unset@|2|
'' opt abc optional_argument "" --abc --abc
|abc@unset@2@unset@|abc@unset@3@unset@|3|
'' opt abc 0 abcd 0 "" --abc
|abc@unset@2@unset@|2|
'' opt abc 0 abd 0 "" --ab
|?@unset@2@option `--ab' is ambiguous@option `--ab' is ambiguous|2|
'' opt abc 0 abcd 0 "" --ab
|?@unset@2@option `--ab' is ambiguous@option `--ab' is ambiguous|2|
'' opt abc 0 abc 1 "" --ab
|abc@unset@2@unset@|2|
'' opt abc 0 abc 1 "" --abc
|abc@unset@2@unset@|2|
'' opt abc 0 abc 1 "" --ab
|abc@unset@2@unset@|2|
'' opt abc 0 acd 0 "" --ab
|abc@unset@2@unset@|2|
abc:d:e::f:: opt ab 0 ac 1 bc 2 cd 1 cde 2 "" -abcdef -a -f -c --a --a= --b=foo -fg
|a@unset@1@unset@|b@unset@1@unset@|c@def@2@unset@|a@unset@3@unset@|f@unset@4@unset@|c@--a@6@unset@|?@unset@7@option `--a' is ambiguous@option `--a' is ambiguous|bc@foo@8@unset@|f@g@9@unset@|9|
: '' '' a
|1|getopts_long: invalid variable name: `'
: 1a ''
|1|getopts_long: invalid variable name: `1a'
- a
|1|getopts_long: invalid option specification: `-'
:a::a:abcd o ab 1 abc 1 abd 1 abe 1 abc 2 '' -aa --ab 1 --abc
|a@a@2@unset@|ab@1@4@unset@|:@abc@5@option `--abc' requires an argument@|5|
:
|1|getopts_long: not enough arguments
'\[$' o -- 0 ' ' 1 '#' required_argument '' '-\\\[$' --\ =a --\#=\$\$
|\@unset@1@unset@|\@unset@1@unset@|\@unset@1@unset@|[@unset@1@unset@|$@unset@2@unset@| @a@3@unset@|#@$$@4@unset@|4|
: o a 1 b 2 c
|1|getopts_long: long option specifications must end in an empty argument
: o a 1 b 2
|1|getopts_long: long option specifications must end in an empty argument
: o a 1 b 2 c 3 '' --c
|1|getopts_long: invalid long option type: `3'
": " o " " 1 '' "- " "-- =1"
| @unset@1@unset@| @unset@2@unset@| @1@3@unset@|3|
: o a 1 '' --c
|:@c@2@bad option: `--c'@|2|
: o a 1 '' --c=foo
|:@c@2@bad option: `--c'@|2|
: o ab 1 ac 1 ad 1 a 1 '' --a=1
|a@1@2@unset@|2|
EOF
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment