libmakepkg: add optional argument support to parseopts

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>
Reviewed-by: Dave Reisner <dreisner@archlinux.org>
Signed-off-by: Allan McRae <allan@archlinux.org>
This commit is contained in:
Ethan Sommer 2019-11-03 19:45:04 -05:00 committed by Allan McRae
parent f6564377a2
commit 7be7552329
2 changed files with 83 additions and 45 deletions

View file

@ -18,16 +18,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
# A getopt_long-like parser which portably supports longopts and # A getopt_long-like parser which portably supports longopts and
# shortopts with some GNU extensions. It does not allow for options # shortopts with some GNU extensions. For both short and long opts,
# with optional arguments. For both short and long opts, options # options requiring an argument should be suffixed with a colon, and
# requiring an argument should be suffixed with a colon. After the # options with optional arguments should be suffixed with a question
# first argument containing the short opts, any number of valid long # mark. After the first argument containing the short opts, any number
# opts may be be passed. The end of the options delimiter must then be # of valid long opts may be be passed. The end of the options delimiter
# added, followed by the user arguments to the calling program. # 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: # Recommended Usage:
# OPT_SHORT='fb:z' # OPT_SHORT='fb:zq?'
# OPT_LONG=('foo' 'bar:' 'baz') # OPT_LONG=('foo' 'bar:' 'baz' 'qux?')
# if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then # if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then
# exit 1 # exit 1
# fi # fi
@ -49,29 +56,30 @@ parseopts() {
longoptmatch() { longoptmatch() {
local o longmatch=() local o longmatch=()
for o in "${longopts[@]}"; do for o in "${longopts[@]}"; do
if [[ ${o%:} = "$1" ]]; then if [[ ${o%[:?]} = "$1" ]]; then
longmatch=("$o") longmatch=("$o")
break break
fi fi
[[ ${o%:} = "$1"* ]] && longmatch+=("$o") [[ ${o%[:?]} = "$1"* ]] && longmatch+=("$o")
done done
case ${#longmatch[*]} in case ${#longmatch[*]} in
1) 1)
# success, override with opt and return arg req (0 == none, 1 == required) # success, override with opt and return arg req (0 == none, 1 == required, 2 == optional)
opt=${longmatch%:} opt=${longmatch%[:?]}
if [[ $longmatch = *: ]]; then case $longmatch in
return 1 *:) return 1 ;;
else *\?) return 2 ;;
return 0 *) return 0 ;;
fi ;; esac
;;
0) 0)
# fail, no match found # fail, no match found
return 255 ;; return 255 ;;
*) *)
# fail, ambiguous match # fail, ambiguous match
printf "${0##*/}: $(gettext "option '%s' is ambiguous; possibilities:")" "--$1" printf "${0##*/}: $(gettext "option '%s' is ambiguous; possibilities:")" "--$1"
printf " '%s'" "${longmatch[@]%:}" printf " '%s'" "${longmatch[@]%[:?]}"
printf '\n' printf '\n'
return 254 ;; return 254 ;;
esac >&2 esac >&2
@ -87,23 +95,16 @@ parseopts() {
for (( i = 1; i < ${#1}; i++ )); do for (( i = 1; i < ${#1}; i++ )); do
opt=${1:i:1} opt=${1:i:1}
# option doesn't exist case $shortopts in
if [[ $shortopts != *$opt* ]]; then
printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2
OPTRET=(--)
return 1
fi
OPTRET+=("-$opt")
# option requires optarg # option requires optarg
if [[ $shortopts = *$opt:* ]]; then *$opt:*)
# if we're not at the end of the option chunk, the rest is the optarg # if we're not at the end of the option chunk, the rest is the optarg
if (( i < ${#1} - 1 )); then if (( i < ${#1} - 1 )); then
OPTRET+=("${1:i+1}") OPTRET+=("-$opt" "${1:i+1}")
break break
# if we're at the end, grab the the next positional, if it exists # if we're at the end, grab the the next positional, if it exists
elif (( i == ${#1} - 1 )) && [[ $2 ]]; then elif (( i == ${#1} - 1 )) && [[ $2 ]]; then
OPTRET+=("$2") OPTRET+=("-$opt" "$2")
shift shift
break break
# parse failure # parse failure
@ -112,7 +113,29 @@ parseopts() {
OPTRET=(--) OPTRET=(--)
return 1 return 1
fi 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 fi
;;
# option has no optarg
*$opt*)
OPTRET+=("-$opt")
;;
# option doesn't exist
*)
printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2
OPTRET=(--)
return 1
;;
esac
done done
;; ;;
--?*=*|--?*) # long option --?*=*|--?*) # long option
@ -145,6 +168,15 @@ parseopts() {
return 1 return 1
fi fi
;; ;;
2)
# --longopt=optarg
if [[ $1 = *=* ]]; then
OPTRET+=("--$opt=$optarg")
# --longopt
else
OPTRET+=("--$opt")
fi
;;
254) 254)
# ambiguous option -- error was reported for us by longoptmatch() # ambiguous option -- error was reported for us by longoptmatch()
OPTRET=(--) OPTRET=(--)

View file

@ -16,12 +16,12 @@ if ! type -t parseopts &>/dev/null; then
fi fi
# borrow opts from makepkg # borrow opts from makepkg
OPT_SHORT="AcdefFghiLmop:rRsV" OPT_SHORT="AcdefFghiLmop:rRsVb?"
OPT_LONG=('allsource' 'asroot' 'ignorearch' 'check' 'clean:' 'cleanall' 'nodeps' OPT_LONG=('allsource' 'asroot' 'ignorearch' 'check' 'clean:' 'cleanall' 'nodeps'
'noextract' 'force' 'forcever:' 'geninteg' 'help' 'holdver' 'noextract' 'force' 'forcever:' 'geninteg' 'help' 'holdver'
'install' 'key:' 'log' 'nocolor' 'nobuild' 'nocheck' 'noprepare' 'nosign' 'pkg:' 'rmdeps' 'install' 'key:' 'log' 'nocolor' 'nobuild' 'nocheck' 'noprepare' 'nosign' 'pkg:' 'rmdeps'
'repackage' 'skipinteg' 'sign' 'source' 'syncdeps' 'version' 'config:' 'repackage' 'skipinteg' 'sign' 'source' 'syncdeps' 'version' 'config:'
'noconfirm' 'noprogressbar') 'noconfirm' 'noprogressbar' 'opt?')
tap_parse() { tap_parse() {
local result=$1 tokencount=$2; shift 2 local result=$1 tokencount=$2; shift 2
@ -31,7 +31,7 @@ tap_parse() {
unset OPTRET unset OPTRET
} }
tap_plan 50 tap_plan 54
# usage: tap_parse <expected result> <token count> test-params... # usage: tap_parse <expected result> <token count> test-params...
# a failed tap_parse will match only the end of options marker '--' # 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) # exact match on possible stem (opt has optarg)
tap_parse '--clean foo --' 3 --clean=foo 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 tap_finish