[pacman-dev] [PATCH v3] libmakepkg: add optional argument support to parseopts
Dave Reisner
d at falconindy.com
Mon Nov 4 00:21:25 UTC 2019
On Wed, Oct 23, 2019 at 08:38:08PM -0400, Ethan Sommer wrote:
> 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 at 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
I think I mentioned it before and I stil think it's weird -- short opts
never use = for associating a value. can we just depend on '-afoo'
meaning '-a' with the optional value 'foo' and drop the equals?
> +
Do these tests really pass? We agreed on '?' rather than ';' as the
symbol for optional.
> tap_finish
> --
> 2.23.0
More information about the pacman-dev
mailing list