1
0
mirror of git://git.code.sf.net/p/zsh/code synced 2024-09-28 06:49:49 +02:00

44100: zparseopts: Add -F option, completion, tests; improve documentation

* Enable zparseopts to perform basic usage validation (aborting on an
  unrecognised option-like parameter)

* Officially document the resolution of ambiguous option specs
This commit is contained in:
dana 2019-03-12 19:01:18 -05:00
parent 36290f3e8e
commit 632023acc2
5 changed files with 268 additions and 13 deletions

View File

@ -1,3 +1,9 @@
2019-03-12 dana <dana@dana.is>
* 44100: Completion/Zsh/Command/_zparseopts, Doc/Zsh/mod_zutil.yo,
Src/Modules/zutil.c, Test/V12zparseopts.ztst: Add `zparseopts -F`,
completion, tests, documentation
2019-03-08 Jun-ichi Takimoto <takimoto-j@kba.biglobe.ne.jp>
* 44101: Completion/Unix/Command/_vim: better support for nvim

View File

@ -0,0 +1,37 @@
#compdef zparseopts
local ret=1
local -a context line state state_descr alts opts
local -A opt_args
_arguments -A '-*' : \
'-a+[specify array in which to store parsed options]:array:_parameters -g "*array*~*readonly*"' \
'-A+[specify association in which to store parsed options]:association:_parameters -g "*association*~*readonly*"' \
'-D[remove parsed options from positional parameters]' \
"-E[don't stop parsing at first parameter not described by specs]" \
'-F[abort parsing and print error at first option-like parameter not described by specs]' \
'-K[preserve contents of arrays/associations when specs are not matched]' \
'-M[enable mapping among equivalent options with opt1=opt2 spec form]' \
'(-)-[end zparseopts options; specs follow]' \
'*: :->spec' \
&& ret=0
[[ $state == spec ]] &&
if compset -P '*='; then
alts=()
(( $+opt_args[-M] )) && {
opts=( $line )
[[ $opts[1] == (-|--) ]] && shift opts
opts=( ${(@)opts%%(+|)(:|:-|::|)(=*|)} )
opts=( ${(@)opts:#${words[CURRENT]%%=*}} )
alts+=( "spec-opt-names:spec option name:(${(j< >)${(@q+)opts}})" )
}
alts+=( 'parameters:array:_parameters -g "*array*~*readonly*"' )
_alternative $alts && ret=0
else
# Not great, but close enough for now
compset -S '=*'
_message -e spec-opts 'spec option (name[+][:|:-|::])' && ret=0
fi
return ret

View File

@ -180,7 +180,7 @@ item(tt(zregexparse))(
This implements some internals of the tt(_regex_arguments) function.
)
findex(zparseopts)
item(tt(zparseopts) [ tt(-D) tt(-K) tt(-M) tt(-E) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-) ] var(spec) ...)(
item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-) ] var(spec) ...)(
This builtin simplifies the parsing of options in positional parameters,
i.e. the set of arguments given by tt($*). Each var(spec) describes one
option and must be of the form `var(opt)[tt(=)var(array)]'. If an option
@ -195,7 +195,7 @@ Note that it is an error to give any var(spec) without an
Unless the tt(-E) option is given, parsing stops at the first string
that isn't described by one of the var(spec)s. Even with tt(-E),
parsing always stops at a positional parameter equal to `tt(-)' or
`tt(-)tt(-)'.
`tt(-)tt(-)'. See also tt(-F).
The var(opt) description must be one of the following. Any of the special
characters can appear in the option name provided it is preceded by a
@ -234,9 +234,23 @@ first colon.
)
enditem()
In all cases, option-arguments must appear either immediately following the
option in the same positional parameter or in the next one. Even an optional
argument may appear in the next parameter, unless it begins with a `tt(-)'.
There is no special handling of `tt(=)' as with GNU-style argument parsers;
given the var(spec) `tt(-foo:)', the positional parameter `tt(-)tt(-foo=bar)'
is parsed as `tt(-)tt(-foo)' with an argument of `tt(=bar)'.
When the names of two options that take no arguments overlap, the longest one
wins, so that parsing for the var(spec)s `tt(-foo -foobar)' (for example) is
unambiguous. However, due to the aforementioned handling of option-arguments,
ambiguities may arise when at least one overlapping var(spec) takes an
argument, as in `tt(-foo: -foobar)'. In that case, the last matching
var(spec) wins.
The options of tt(zparseopts) itself cannot be stacked because, for
example, the stack `tt(-DEK)' is indistinguishable from a var(spec) for
the GNU-style long option `tt(--DEK)'. The options of tt(zparseopts)
the GNU-style long option `tt(-)tt(-DEK)'. The options of tt(zparseopts)
itself are:
startitem()
@ -252,8 +266,29 @@ as the values.
item(tt(-D))(
If this option is given, all options found are removed from the positional
parameters of the calling shell or shell function, up to but not including
any not described by the var(spec)s. This is similar to using the tt(shift)
builtin.
any not described by the var(spec)s. If the first such parameter is `tt(-)'
or `tt(-)tt(-)', it is removed as well. This is similar to using the
tt(shift) builtin.
)
item(tt(-E))(
This changes the parsing rules to em(not) stop at the first string
that isn't described by one of the var(spec)s. It can be used to test
for or (if used together with tt(-D)) extract options and their
arguments, ignoring all other options and arguments that may be in the
positional parameters. As indicated above, parsing still stops at the
first `tt(-)' or `tt(-)tt(-)' not described by a var(spec), but it is not
removed when used with tt(-D).
)
item(tt(-F))(
If this option is given, tt(zparseopts) immediately stops at the first
option-like parameter not described by one of the var(spec)s, prints an
error message, and returns status 1. Removal (tt(-D)) and extraction
(tt(-E)) are not performed, and option arrays are not updated. This
provides basic validation for the given options.
Note that the appearance in the positional parameters of an option without
its required argument always aborts parsing and returns an error as described
above regardless of whether this option is used.
)
item(tt(-K))(
With this option, the arrays specified with the tt(-a) option and with the
@ -272,13 +307,6 @@ is found, the values are stored as usual. This changes only the way the
values are stored, not the way tt($*) is parsed, so results may be
unpredictable if the `var(name)tt(+)' specifier is used inconsistently.
)
item(tt(-E))(
This changes the parsing rules to em(not) stop at the first string
that isn't described by one of the var(spec)s. It can be used to test
for or (if used together with tt(-D)) extract options and their
arguments, ignoring all other options and arguments that may be in the
positional parameters.
)
enditem()
For example,

View File

@ -1644,7 +1644,7 @@ static int
bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
{
char *o, *p, *n, **pp, **aval, **ap, *assoc = NULL, **cp, **np;
int del = 0, flags = 0, extract = 0, keep = 0;
int del = 0, flags = 0, extract = 0, fail = 0, keep = 0;
Zoptdesc sopts[256], d;
Zoptarr a, defarr = NULL;
Zoptval v;
@ -1681,6 +1681,14 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
}
extract = 1;
break;
case 'F':
if (o[2]) {
args--;
o = NULL;
break;
}
fail = 1;
break;
case 'K':
if (o[2]) {
args--;
@ -1843,6 +1851,10 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
if (!(d = lookup_opt(o + 1))) {
while (*++o) {
if (!(d = sopts[STOUC(*o)])) {
if (fail) {
zwarnnam(nam, "bad option: %c", *o);
return 1;
}
o = NULL;
break;
}

172
Test/V12zparseopts.ztst Normal file
View File

@ -0,0 +1,172 @@
# Test zparseopts from the zsh/zutil module
%prep
if zmodload zsh/zutil 2> /dev/null; then
# Produce a string representing an associative array ordered by its keys
order_assoc() {
local -a _arr
for 2 in "${(@kP)1}"; do
_arr+=( "${(q-)2} ${(q-)${(P)1}[$2]}" )
done
print -r - ${(j< >)${(@o)_arr}}
}
else
ZTST_unimplemented="can't load the zsh/zutil module for testing"
fi
%test
() {
local -a optv
zparseopts -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} -ab1 -c -d -e -z
0:zparseopts -a
>ret: 0, optv: -a -b 1 -c-d, argv: -ab1 -c -d -e -z
() {
local -A opts
zparseopts -A opts - a b: c:- z
print -r - ret: $?, opts: "$( order_assoc opts )", argv: $argv
} -ab1 -c -d -e -z
0:zparseopts -A
>ret: 0, opts: -a '' -b 1 -c -d, argv: -ab1 -c -d -e -z
() {
local -a optv
zparseopts -D -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} -ab1 -c -d -e -z
0:zparseopts -D
>ret: 0, optv: -a -b 1 -c-d, argv: -e -z
() {
local -a optv
zparseopts -E -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} -ab1 -c -d -e -z
0:zparseopts -E
>ret: 0, optv: -a -b 1 -c-d -z, argv: -ab1 -c -d -e -z
() {
local -a optv
zparseopts -D -E -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} -ab1 -c -d -e -z
0:zparseopts -D -E
>ret: 0, optv: -a -b 1 -c-d -z, argv: -e
for 1 in '-a -x -z' '-ax -z' '-a --x -z'; do
() {
local -a optv
zparseopts -D -E -F -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} $=1
done
0:zparseopts -F
?(anon):zparseopts:2: bad option: x
>ret: 1, optv: , argv: -a -x -z
?(anon):zparseopts:2: bad option: x
>ret: 1, optv: , argv: -ax -z
?(anon):zparseopts:2: bad option: -
>ret: 1, optv: , argv: -a --x -z
for 1 in '-a 1 2 3' '1 2 3'; do
() {
local -a optv=( -x -y -z )
zparseopts -D -K -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} $=1
done
0:zparseopts -K -a
>ret: 0, optv: -a, argv: 1 2 3
>ret: 0, optv: -x -y -z, argv: 1 2 3
for 1 in '-a 1 2 3' '1 2 3'; do
() {
local -A opts=( -b 1 -z '' )
zparseopts -D -K -A opts - a b: c:- z
print -r - ret: $?, opts: "$( order_assoc opts )", argv: $argv
} $=1
done
0:zparseopts -K -A
>ret: 0, opts: -a '' -b 1 -z '', argv: 1 2 3
>ret: 0, opts: -b 1 -z '', argv: 1 2 3
() {
local -a optv
local -A opts
zparseopts -D -M -a optv -A opts - a:=-aaa -aaa:
print -r - ret: $?, optv: $optv, opts: "$( order_assoc opts )", argv: $argv
} --aaa foo -a bar 1 2 3
0:zparseopts -M
>ret: 0, optv: --aaa bar, opts: --aaa bar, argv: 1 2 3
() {
local -a optv aa ab
zparseopts -a optv - a=aa b:=ab c:- z
print -r - ret: $?, optv: $optv, aa: $aa, ab: $ab, argv: $argv
} -ab1 -c -d
0:multiple arrays
>ret: 0, optv: -c-d, aa: -a, ab: -b 1, argv: -ab1 -c -d
for 1 in '-a - -b - - -b' '-a -- -b -- -- -b' '-a 1 -b - - -b'; do
# -D alone strips - out
() {
local -a optv
zparseopts -D -F -a optv - a b: c:- z
print -r - '(-D )' ret: $?, optv: $optv, argv: $argv
} $=1
# -D -E leaves - in
() {
local -a optv
zparseopts -D -E -F -a optv - a b: c:- z
print -r - '(-D -E)' ret: $?, optv: $optv, argv: $argv
} $=1
done
0:-/-- handling
>(-D ) ret: 0, optv: -a, argv: -b - - -b
>(-D -E) ret: 0, optv: -a, argv: - -b - - -b
>(-D ) ret: 0, optv: -a, argv: -b -- -- -b
>(-D -E) ret: 0, optv: -a, argv: -- -b -- -- -b
>(-D ) ret: 0, optv: -a, argv: 1 -b - - -b
>(-D -E) ret: 0, optv: -a -b -, argv: 1 - -b
# Escaping should always work, but it's optional on the first character
for specs in '\+ \: \= \\' '+ : = \'; do
() {
local -a optv
zparseopts -D -a optv - $=specs
print -r - ret: $?, optv: $optv, argv: $argv
} -+:=\\ 1 2 3
done
() {
local -a optv
zparseopts -D -a optv - '-\:\:\::'
print -r - ret: $?, optv: $optv, argv: $argv
} --:::foo 1 2 3
0:special characters in option names
>ret: 0, optv: -+ -: -= -\, argv: 1 2 3
>ret: 0, optv: -+ -: -= -\, argv: 1 2 3
>ret: 0, optv: --::: foo, argv: 1 2 3
for specs in '-foo: -foobar' '-foobar -foo:'; do
() {
local -a optv
zparseopts -a optv - $=specs
print -r - ret: $?, optv: $optv, argv: $argv
} --foobar 1 2 3
done
0:overlapping option specs (scan order)
>ret: 0, optv: --foobar, argv: --foobar 1 2 3
>ret: 0, optv: --foo bar, argv: --foobar 1 2 3
() {
local -a optv
zparseopts -a optv - a b: c:- z
print -r - ret: $?, optv: $optv, argv: $argv
} -ab1 -c
0:missing optarg
?(anon):zparseopts:2: missing argument for option: c
>ret: 1, optv: , argv: -ab1 -c