scripts/library: introduce parseopts
This will replace our current options parser used in pacman-key, makepkg, and ideally elsewhere. It follows heuristics closer to that of GNU getopt long (and thus pacman itself), with the exception that it does not allow for options with optional arguments. Due to the way this parser will be used, this sort of functionality will not be needed. Instead of relying on eval+set, options are normalized into an array, OPTRET, which callers should expect to be populated after returning from parseopts. This avoids problems with quotes and spaces in arguments, assuming that the user quotes properly when passing into the application. A new test harness for parseopts is added in test/scripts. Signed-off-by: Dave Reisner <dreisner@archlinux.org>
This commit is contained in:
parent
1eb6a9cbfe
commit
8679cd68d8
|
@ -1,4 +1,4 @@
|
|||
SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/pacman test/util
|
||||
SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/pacman test/util test/scripts
|
||||
if WANT_DOC
|
||||
SUBDIRS += doc
|
||||
endif
|
||||
|
@ -23,7 +23,7 @@ dist_pkgdata_DATA = \
|
|||
proto/ChangeLog.proto
|
||||
|
||||
# run the pactest test suite and vercmp tests
|
||||
check-local: test/pacman test/util src/pacman src/util
|
||||
check-local: test/pacman test/scripts test/util src/pacman src/util
|
||||
LC_ALL=C $(PYTHON) $(top_srcdir)/test/pacman/pactest.py --debug=1 \
|
||||
--test $(top_srcdir)/test/pacman/tests/*.py \
|
||||
-p $(top_builddir)/src/pacman/pacman
|
||||
|
@ -31,6 +31,8 @@ check-local: test/pacman test/util src/pacman src/util
|
|||
$(top_builddir)/src/util/pacsort
|
||||
$(SH) $(top_srcdir)/test/util/vercmptest.sh \
|
||||
$(top_builddir)/src/util/vercmp
|
||||
$(BASH_SHELL) $(top_srcdir)/test/scripts/parseopts_test.sh \
|
||||
$(top_srcdir)/scripts/library/parseopts.sh
|
||||
|
||||
# create the pacman DB and cache directories upon install
|
||||
install-data-local:
|
||||
|
|
|
@ -442,6 +442,7 @@ doc/Makefile
|
|||
etc/Makefile
|
||||
test/pacman/Makefile
|
||||
test/pacman/tests/Makefile
|
||||
test/scripts/Makefile
|
||||
test/util/Makefile
|
||||
contrib/Makefile
|
||||
Makefile
|
||||
|
|
|
@ -27,6 +27,7 @@ EXTRA_DIST = \
|
|||
|
||||
LIBRARY = \
|
||||
library/output_format.sh \
|
||||
library/parseopts.sh \
|
||||
library/parse_options.sh
|
||||
|
||||
# Files that should be removed, but which Automake does not know.
|
||||
|
|
|
@ -13,3 +13,23 @@ A getopt replacement to avoids portability issues, in particular the
|
|||
lack of long option name support in the default getopt provided by some
|
||||
platforms.
|
||||
Usage: parse_option $SHORT_OPTS $LONG_OPTS "$@"
|
||||
|
||||
parseopts.sh:
|
||||
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.
|
||||
|
||||
Reccommended Usage:
|
||||
OPT_SHORT='fb:z'
|
||||
OPT_LONG=('foo' 'bar:' 'baz')
|
||||
if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then
|
||||
exit 1
|
||||
fi
|
||||
set -- "${OPTRET[@]}"
|
||||
Returns:
|
||||
0: parse success
|
||||
1: parse failure (error message supplied)
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
# getopt-like parser
|
||||
parseopts() {
|
||||
local opt= optarg= i= shortopts=$1
|
||||
local -a longopts=() unused_argv=()
|
||||
|
||||
shift
|
||||
while [[ $1 && $1 != '--' ]]; do
|
||||
longopts+=("$1")
|
||||
shift
|
||||
done
|
||||
shift
|
||||
|
||||
longoptmatch() {
|
||||
local o longmatch=()
|
||||
for o in "${longopts[@]}"; do
|
||||
if [[ ${o%:} = "$1" ]]; then
|
||||
longmatch=("$o")
|
||||
break
|
||||
fi
|
||||
[[ ${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 ;;
|
||||
0)
|
||||
# fail, no match found
|
||||
return 255 ;;
|
||||
*)
|
||||
# fail, ambiguous match
|
||||
printf "@SCRIPTNAME@: $(gettext "option '%s' is ambiguous; possibilities:")" "--$1"
|
||||
printf " '%s'" "${longmatch[@]%:}"
|
||||
printf '\n'
|
||||
return 254 ;;
|
||||
esac >&2
|
||||
}
|
||||
|
||||
while (( $# )); do
|
||||
case $1 in
|
||||
--) # explicit end of options
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-[!-]*) # short option
|
||||
for (( i = 1; i < ${#1}; i++ )); do
|
||||
opt=${1:i:1}
|
||||
|
||||
# option doesn't exist
|
||||
if [[ $shortopts != *$opt* ]]; then
|
||||
printf "@SCRIPTNAME@: $(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 "@SCRIPTNAME@: $(gettext "option requires an argument") -- '%s'\n" "$opt" >&2
|
||||
OPTRET=(--)
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
;;
|
||||
--?*=*|--?*) # long option
|
||||
IFS='=' read -r opt optarg <<< "${1#--}"
|
||||
longoptmatch "$opt"
|
||||
case $? in
|
||||
0)
|
||||
# parse failure
|
||||
if [[ $optarg ]]; then
|
||||
printf "@SCRIPTNAME@: $(gettext "option '%s' does not allow an argument")\n" "--$opt" >&2
|
||||
OPTRET=(--)
|
||||
return 1
|
||||
# --longopt
|
||||
else
|
||||
OPTRET+=("--$opt")
|
||||
shift
|
||||
continue 2
|
||||
fi
|
||||
;;
|
||||
1)
|
||||
# --longopt=optarg
|
||||
if [[ $optarg ]]; then
|
||||
OPTRET+=("--$opt" "$optarg")
|
||||
shift
|
||||
# --longopt optarg
|
||||
elif [[ $2 ]]; then
|
||||
OPTRET+=("--$opt" "$2" )
|
||||
shift 2
|
||||
# parse failure
|
||||
else
|
||||
printf "@SCRIPTNAME@: $(gettext "option '%s' requires an argument")\n" "--$opt" >&2
|
||||
OPTRET=(--)
|
||||
return 1
|
||||
fi
|
||||
continue 2
|
||||
;;
|
||||
254)
|
||||
# ambiguous option -- error was reported for us by longoptmatch()
|
||||
OPTRET=(--)
|
||||
return 1
|
||||
;;
|
||||
255)
|
||||
# parse failure
|
||||
printf "@SCRIPTNAME@: $(gettext "invalid option") '--%s'\n" "$opt" >&2
|
||||
OPTRET=(--)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*) # non-option arg encountered, add it as a parameter
|
||||
unused_argv+=("$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# add end-of-opt terminator and any leftover positional parameters
|
||||
OPTRET+=('--' "${unused_argv[@]}" "$@")
|
||||
unset longoptmatch
|
||||
|
||||
return 0
|
||||
}
|
|
@ -9,3 +9,4 @@ scripts/pkgdelta.sh.in
|
|||
scripts/repo-add.sh.in
|
||||
scripts/library/output_format.sh
|
||||
scripts/library/parse_options.sh
|
||||
scripts/library/parseopts.sh
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
check_SCRIPTS = \
|
||||
parseopts_test.sh
|
||||
|
||||
noinst_SCRIPTS = $(check_SCRIPTS)
|
||||
|
||||
EXTRA_DIST = \
|
||||
$(check_SCRIPTS)
|
||||
|
||||
# vim:set ts=2 sw=2 noet:
|
|
@ -0,0 +1,138 @@
|
|||
#!/bin/bash
|
||||
|
||||
declare -i testcount=0 pass=0 fail=0
|
||||
|
||||
# source the library function
|
||||
if [[ -z $1 || ! -f $1 ]]; then
|
||||
printf "error: path to parseopts library not provided or does not exist\n"
|
||||
exit 1
|
||||
fi
|
||||
. "$1"
|
||||
|
||||
if ! type -t parseopts >/dev/null; then
|
||||
printf 'parseopts function not found\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# borrow opts from makepkg
|
||||
OPT_SHORT="AcdefFghiLmop:rRsV"
|
||||
OPT_LONG=('allsource' 'asroot' 'ignorearch' 'check' 'clean:' 'cleanall' 'nodeps'
|
||||
'noextract' 'force' 'forcever:' 'geninteg' 'help' 'holdver'
|
||||
'install' 'key:' 'log' 'nocolor' 'nobuild' 'nocheck' 'nosign' 'pkg:' 'rmdeps'
|
||||
'repackage' 'skipinteg' 'sign' 'source' 'syncdeps' 'version' 'config:'
|
||||
'noconfirm' 'noprogressbar')
|
||||
|
||||
parse() {
|
||||
local result=$1 tokencount=$2; shift 2
|
||||
|
||||
(( ++testcount ))
|
||||
parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@" 2>/dev/null
|
||||
test_result "$result" "$tokencount" "$*" "${OPTRET[@]}"
|
||||
unset OPTRET
|
||||
}
|
||||
|
||||
test_result() {
|
||||
local result=$1 tokencount=$2 input=$3; shift 3
|
||||
|
||||
if [[ $result = "$*" ]] && (( tokencount == $# )); then
|
||||
(( ++pass ))
|
||||
else
|
||||
printf '[TEST %3s]: FAIL\n' "$testcount"
|
||||
printf ' input: %s\n' "$input"
|
||||
printf ' output: %s (%s tokens)\n' "$*" "$#"
|
||||
printf ' expected: %s (%s tokens)\n' "$result" "$tokencount"
|
||||
echo
|
||||
(( ++fail ))
|
||||
fi
|
||||
}
|
||||
|
||||
summarize() {
|
||||
if (( !fail )); then
|
||||
printf 'All %s tests successful\n' "$testcount"
|
||||
exit 0
|
||||
else
|
||||
printf '%s of %s tests failed\n' "$fail" "$testcount"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
trap 'summarize' EXIT
|
||||
|
||||
printf 'Beginning parseopts tests\n'
|
||||
|
||||
# usage: parse <expected result> <token count> test-params...
|
||||
# a failed parse will match only the end of options marker '--'
|
||||
|
||||
# no options
|
||||
parse '--' 1
|
||||
|
||||
# short options
|
||||
parse '-s -r --' 3 -s -r
|
||||
|
||||
# short options, no spaces
|
||||
parse '-s -r --' 3 -sr
|
||||
|
||||
# short opt missing an opt arg
|
||||
parse '--' 1 -s -p
|
||||
|
||||
# short opt with an opt arg
|
||||
parse '-p PKGBUILD -L --' 4 -p PKGBUILD -L
|
||||
|
||||
# short opt with an opt arg, no space
|
||||
parse '-p PKGBUILD --' 3 -pPKGBUILD
|
||||
|
||||
# valid shortopts as a long opt
|
||||
parse '--' 1 --sir
|
||||
|
||||
# long opt wiht no optarg
|
||||
parse '--log --' 2 --log
|
||||
|
||||
# long opt with missing optarg
|
||||
parse '--' 1 -sr --pkg
|
||||
|
||||
# long opt with optarg
|
||||
parse '--pkg foo --' 3 --pkg foo
|
||||
|
||||
# long opt with optarg with whitespace
|
||||
parse '--pkg foo bar -- baz' 4 --pkg "foo bar" baz
|
||||
|
||||
# long opt with optarg with =
|
||||
parse '--pkg foo=bar -- baz' 4 --pkg foo=bar baz
|
||||
|
||||
# long opt with explicit optarg
|
||||
parse '--pkg bar -- foo baz' 5 foo --pkg=bar baz
|
||||
|
||||
# long opt with explicit optarg, with whitespace
|
||||
parse '--pkg foo bar -- baz' 4 baz --pkg="foo bar"
|
||||
|
||||
# long opt with explicit optarg that doesn't take optarg
|
||||
parse '--' 1 --force=always -s
|
||||
|
||||
# long opt with explicit optarg with =
|
||||
parse '--pkg foo=bar --' 3 --pkg=foo=bar
|
||||
|
||||
# explicit end of options with options after
|
||||
parse '-s -r -- foo bar baz' 6 -s -r -- foo bar baz
|
||||
|
||||
# non-option parameters mixed in with options
|
||||
parse '-s -r -- foo baz' 5 -s foo baz -r
|
||||
|
||||
# optarg with whitespace
|
||||
parse '-p foo bar -s --' 4 -p'foo bar' -s
|
||||
|
||||
# non-option parameter with whitespace
|
||||
parse '-i -- foo bar' 3 -i 'foo bar'
|
||||
|
||||
# successful stem match (opt has no arg)
|
||||
parse '--nocolor --' 2 --nocol
|
||||
|
||||
# successful stem match (opt has arg)
|
||||
parse '--config foo --' 3 --conf foo
|
||||
|
||||
# ambiguous long opt
|
||||
parse '--' 1 '--for'
|
||||
|
||||
# exact match on a possible stem (--force & --forcever)
|
||||
parse '--force --' 2 --force
|
||||
|
||||
# exact match on possible stem (opt has optarg)
|
||||
parse '--clean foo --' 3 --clean=foo
|
Loading…
Reference in New Issue