Adds a "?" suffix that can be used to indicate that an option's argument is optional. This allows options to have a default behaviour when the user doesn't specify one, e.g.: --color=[when] being able to behave like --color=auto when only --color is passed Options with optional arguments given on the command line will be returned in the form "--opt=optarg" and "-o=optarg". Despite that not being the syntax for passing an argument with a shortopt (trying to pass -o=foo would make -o's argument "=foo"), this is done to allow the caller to split the option and its optarg easily Signed-off-by: Ethan Sommer <e5ten.arch@gmail.com> --- scripts/libmakepkg/util/parseopts.sh.in | 116 +++++++++++++++--------- test/scripts/parseopts_test.sh | 12 ++- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/scripts/libmakepkg/util/parseopts.sh.in b/scripts/libmakepkg/util/parseopts.sh.in index c056cb1e..42540438 100644 --- a/scripts/libmakepkg/util/parseopts.sh.in +++ b/scripts/libmakepkg/util/parseopts.sh.in @@ -18,16 +18,23 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # # A getopt_long-like parser which portably supports longopts and -# shortopts with some GNU extensions. It does not allow for options -# with optional arguments. For both short and long opts, options -# requiring an argument should be suffixed with a colon. After the -# first argument containing the short opts, any number of valid long -# opts may be be passed. The end of the options delimiter must then be -# added, followed by the user arguments to the calling program. +# shortopts with some GNU extensions. For both short and long opts, +# options requiring an argument should be suffixed with a colon, and +# options with optional arguments should be suffixed with a question +# mark. After the first argument containing the short opts, any number +# of valid long opts may be be passed. The end of the options delimiter +# must then be added, followed by the user arguments to the calling +# program. +# +# Options with optional arguments will be returned as "--longopt=optarg" +# for longopts, or "-o=optarg" for shortopts. This isn't actually a valid +# way to pass an optional argument with a shortopt on the command line, +# but is done by parseopts to enable the caller script to split the option +# and its optarg easily. # # Recommended Usage: -# OPT_SHORT='fb:z' -# OPT_LONG=('foo' 'bar:' 'baz') +# OPT_SHORT='fb:zq?' +# OPT_LONG=('foo' 'bar:' 'baz' 'qux?') # if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then # exit 1 # fi @@ -49,29 +56,30 @@ parseopts() { longoptmatch() { local o longmatch=() for o in "${longopts[@]}"; do - if [[ ${o%:} = "$1" ]]; then + if [[ ${o%[:?]} = "$1" ]]; then longmatch=("$o") break fi - [[ ${o%:} = "$1"* ]] && longmatch+=("$o") + [[ ${o%[:?]} = "$1"* ]] && longmatch+=("$o") done case ${#longmatch[*]} in 1) - # success, override with opt and return arg req (0 == none, 1 == required) - opt=${longmatch%:} - if [[ $longmatch = *: ]]; then - return 1 - else - return 0 - fi ;; + # success, override with opt and return arg req (0 == none, 1 == required, 2 == optional) + opt=${longmatch%[:?]} + case $longmatch in + *:) return 1 ;; + *\?) return 2 ;; + *) return 0 ;; + esac + ;; 0) # fail, no match found return 255 ;; *) # fail, ambiguous match printf "${0##*/}: $(gettext "option '%s' is ambiguous; possibilities:")" "--$1" - printf " '%s'" "${longmatch[@]%:}" + printf " '%s'" "${longmatch[@]%[:?]}" printf '\n' return 254 ;; esac >&2 @@ -87,32 +95,47 @@ parseopts() { for (( i = 1; i < ${#1}; i++ )); do opt=${1:i:1} - # option doesn't exist - if [[ $shortopts != *$opt* ]]; then - printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2 - OPTRET=(--) - return 1 - fi - - OPTRET+=("-$opt") - # option requires optarg - if [[ $shortopts = *$opt:* ]]; then - # if we're not at the end of the option chunk, the rest is the optarg - if (( i < ${#1} - 1 )); then - OPTRET+=("${1:i+1}") - break - # if we're at the end, grab the the next positional, if it exists - elif (( i == ${#1} - 1 )) && [[ $2 ]]; then - OPTRET+=("$2") - shift - break - # parse failure - else - printf "${0##*/}: $(gettext "option requires an argument") -- '%s'\n" "$opt" >&2 + case $shortopts in + # option requires optarg + *$opt:*) + # if we're not at the end of the option chunk, the rest is the optarg + if (( i < ${#1} - 1 )); then + OPTRET+=("-$opt" "${1:i+1}") + break + # if we're at the end, grab the the next positional, if it exists + elif (( i == ${#1} - 1 )) && [[ $2 ]]; then + OPTRET+=("-$opt" "$2") + shift + break + # parse failure + else + printf "${0##*/}: $(gettext "option requires an argument") -- '%s'\n" "$opt" >&2 + OPTRET=(--) + return 1 + fi + ;; + # option's optarg is optional + *$opt\?*) + # if we're not at the end of the option chunk, the rest is the optarg + if (( i < ${#1} - 1 )); then + OPTRET+=("-$opt=${1:i+1}") + break + # option has no optarg + else + OPTRET+=("-$opt") + fi + ;; + # option has no optarg + *$opt*) + OPTRET+=("-$opt") + ;; + # option doesn't exist + *) + printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2 OPTRET=(--) return 1 - fi - fi + ;; + esac done ;; --?*=*|--?*) # long option @@ -145,6 +168,15 @@ parseopts() { return 1 fi ;; + 2) + # --longopt=optarg + if [[ $1 = *=* ]]; then + OPTRET+=("--$opt=$optarg") + # --longopt + else + OPTRET+=("--$opt") + fi + ;; 254) # ambiguous option -- error was reported for us by longoptmatch() OPTRET=(--) diff --git a/test/scripts/parseopts_test.sh b/test/scripts/parseopts_test.sh index 9674c6a6..7d14bf29 100755 --- a/test/scripts/parseopts_test.sh +++ b/test/scripts/parseopts_test.sh @@ -16,12 +16,12 @@ if ! type -t parseopts &>/dev/null; then fi # borrow opts from makepkg -OPT_SHORT="AcdefFghiLmop:rRsV" +OPT_SHORT="AcdefFghiLmop:rRsVb;" OPT_LONG=('allsource' 'asroot' 'ignorearch' 'check' 'clean:' 'cleanall' 'nodeps' 'noextract' 'force' 'forcever:' 'geninteg' 'help' 'holdver' 'install' 'key:' 'log' 'nocolor' 'nobuild' 'nocheck' 'noprepare' 'nosign' 'pkg:' 'rmdeps' 'repackage' 'skipinteg' 'sign' 'source' 'syncdeps' 'version' 'config:' - 'noconfirm' 'noprogressbar') + 'noconfirm' 'noprogressbar' 'opt;') tap_parse() { local result=$1 tokencount=$2; shift 2 @@ -31,7 +31,7 @@ tap_parse() { unset OPTRET } -tap_plan 50 +tap_plan 54 # usage: tap_parse <expected result> <token count> test-params... # a failed tap_parse will match only the end of options marker '--' @@ -111,4 +111,10 @@ tap_parse '--force --' 2 --force # exact match on possible stem (opt has optarg) tap_parse '--clean foo --' 3 --clean=foo +# long opt with empty, non-empty, and no optional arg +tap_parse '--opt= --opt=foo --opt --' 4 --opt= --opt=foo --opt + +# short opt with and without optional arg, and non-option arg +tap_parse '-b=foo -A -b -- foo' 5 -bfoo -Ab foo + tap_finish -- 2.23.0