From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on inbox.vuxu.org X-Spam-Level: X-Spam-Status: No, score=-1.0 required=5.0 tests=DKIMWL_WL_MED,DKIM_SIGNED, DKIM_VALID,MAILING_LIST_MULTI,RCVD_IN_DNSWL_NONE autolearn=ham autolearn_force=no version=3.4.2 Received: from primenet.com.au (ns1.primenet.com.au [203.24.36.2]) by inbox.vuxu.org (OpenSMTPD) with ESMTP id 8c6f92de for ; Wed, 6 Mar 2019 18:56:15 +0000 (UTC) Received: (qmail 9692 invoked by alias); 6 Mar 2019 18:55:56 -0000 Mailing-List: contact zsh-workers-help@zsh.org; run by ezmlm Precedence: bulk X-No-Archive: yes List-Id: Zsh Workers List List-Post: List-Help: List-Unsubscribe: X-Seq: 44100 Received: (qmail 288 invoked by uid 1010); 6 Mar 2019 18:55:56 -0000 X-Qmail-Scanner-Diagnostics: from mail-io1-f66.google.com by f.primenet.com.au (envelope-from , uid 7791) with qmail-scanner-2.11 (clamdscan: 0.100.2/25376. spamassassin: 3.4.2. Clear:RC:0(209.85.166.66):SA:0(-1.9/5.0):. Processed in 2.501434 secs); 06 Mar 2019 18:55:56 -0000 X-Envelope-From: dana@dana.is X-Qmail-Scanner-Mime-Attachments: | X-Qmail-Scanner-Zip-Files: | Received-SPF: pass (ns1.primenet.com.au: SPF record at _netblocks.google.com designates 209.85.166.66 as permitted sender) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=dana-is.20150623.gappssmtp.com; s=20150623; h=from:content-transfer-encoding:mime-version:subject:message-id:date :to; bh=39mT/r044cE/0TSWYHpx9tJqAR4++EG3rjdY0JCPk2g=; b=ZK7+AhgCDNPOPBzQUtKLhLr635qZnm4lgL0GOCgyAra5/D8tJstQC+c2FEOLmJ/3cF NStjVVGFk7zzwzGilJSeHx2HC95bzKtE2VC+NVUAukFEhYLPut2tUBAP3X7X2RypT7ck wrqbiNchl2i0X37saLu57OH4thOn5UNmUm4EQKqXm/xAE0U9IaCpl7EUbJisgD8+NFsr WNU+RQYS3/zBSpIZy5uZf9/DQ/8zd/YnG8rXa0O/hfgfrA23b5MRLu0/7MVt3qNXr+JU Sh55SGCR1oImrznhiWaqgDjXhidhnIuNZ24wiTzERQTkOXpgOH7zQt1sNyHrFYXiMyIJ REyQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:content-transfer-encoding:mime-version :subject:message-id:date:to; bh=39mT/r044cE/0TSWYHpx9tJqAR4++EG3rjdY0JCPk2g=; b=aPp+a7+abRBtE8q+pmEa6KmmPhYCiA+66Pp0esMWVhfP/VPu8c/c7EagZtRpyJfoc9 R8hCDXccq3aPx7G/TI1f8x8tKv/hylxs5oJ4I7V4rAuwGHtgjiNkz7P82BQuypXlFqpL RAynloB30MJ4XiHQGUYuRafbUitO7x877eYKAnusndjAeBnrmeg9uAFZsC4wMNnKCLzN BknFV9x3XHthvd8IwyiJYl+E1LR6oVqt/oIz9zrHK6GXj0PISPksc+uDAu+I/vM9SmKB HdkS/NFzFvke1nD6GJmMFoHSDschqvWdTDmxa8mIxGUYzihOjjB9msbq1q4x/qwj0kE6 P1Fw== X-Gm-Message-State: APjAAAUNbfZcWrnUeUwHFpG9vtMHOJCaPvvLzQ89m50KB+/Pm8DIa3GO KIx55EoZVqDKWdLh06yrsQ71x+qu7HbDtQ== X-Google-Smtp-Source: APXvYqzkJ4LAFq5xOdgSti3v52RaPzTva6D0u6LOfJMGtqP3Tw9XJCVWg8r2xXz/IiGnl/EzntbC8g== X-Received: by 2002:a6b:e202:: with SMTP id z2mr4083875ioc.133.1551898518767; Wed, 06 Mar 2019 10:55:18 -0800 (PST) From: dana Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable Mime-Version: 1.0 (Mac OS X Mail 11.5 \(3445.9.1\)) Subject: [PATCH] zparseopts: Add -F option, completion, tests; improve documentation Message-Id: <51A8B624-FE01-44EB-B5F2-23545C831FDA@dana.is> Date: Wed, 6 Mar 2019 12:55:17 -0600 To: Zsh hackers list X-Mailer: Apple Mail (2.3445.9.1) This adds an -F option to zparseopts which causes it to immediately = abort and print an error when it encounters an unrecognised option-like parameter, similar to what it does when a required optarg isn't found. The desire = for this feature has come up in users/9361 and users/23776, as well as on = IRC once or twice, and it seems easy enough to do. (It's possible to do it = reliably in 'user land', too, but it's slow and boiler-plate-y.) Since i was in there, i also added a completion function and a bunch of = tests, and i clarified several things in the documentation. I went through the mailing list looking for edge cases and other stuff that people had = asked about; most notably, i made the handling of ambiguous (overlapping) = option specs 'official', as suggested by Bart in users/17001. To be clear, this = is setting previously unspecified behaviour in stone; please let me know if = you'd rather i keep that bit out after all (or if you see any other issues, = obv) dana diff --git a/Completion/Zsh/Command/_zparseopts = b/Completion/Zsh/Command/_zparseopts new file mode 100644 index 000000000..e13a91081 --- /dev/null +++ b/Completion/Zsh/Command/_zparseopts @@ -0,0 +1,37 @@ +#compdef zparseopts + +local ret=3D1 +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=3Dopt2 spec = form]' \ + '(-)-[end zparseopts options; specs follow]' \ + '*: :->spec' \ +&& ret=3D0 + +[[ $state =3D=3D spec ]] && +if compset -P '*=3D'; then + alts=3D() + (( $+opt_args[-M] )) && { + opts=3D( $line ) + [[ $opts[1] =3D=3D (-|--) ]] && shift opts + opts=3D( ${(@)opts%%(+|)(:|:-|::|)(=3D*|)} ) + opts=3D( ${(@)opts:#${words[CURRENT]%%=3D*}} ) + alts+=3D( "spec-opt-names:spec option name:(${(j< >)${(@q+)opts}})" = ) + } + alts+=3D( 'parameters:array:_parameters -g "*array*~*readonly*"' ) + _alternative $alts && ret=3D0 +else + # Not great, but close enough for now + compset -S '=3D*' + _message -e spec-opts 'spec option (name[+][:|:-|::])' && ret=3D0 +fi + +return ret diff --git a/Doc/Zsh/mod_zutil.yo b/Doc/Zsh/mod_zutil.yo index 15f6ed365..fa1f7b3ea 100644 --- a/Doc/Zsh/mod_zutil.yo +++ b/Doc/Zsh/mod_zutil.yo @@ -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(=3D)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). =20 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() =20 +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(=3D)' as with GNU-style argument = parsers; +given the var(spec) `tt(-foo:)', the positional parameter = `tt(-)tt(-foo=3Dbar)' +is parsed as `tt(-)tt(-foo)' with an argument of `tt(=3Dbar)'. + +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: =20 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() =20 For example, diff --git a/Src/Modules/zutil.c b/Src/Modules/zutil.c index 19a8306b5..c4fe4a15e 100644 --- a/Src/Modules/zutil.c +++ b/Src/Modules/zutil.c @@ -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 =3D NULL, **cp, **np; - int del =3D 0, flags =3D 0, extract =3D 0, keep =3D 0; + int del =3D 0, flags =3D 0, extract =3D 0, fail =3D 0, keep =3D 0; Zoptdesc sopts[256], d; Zoptarr a, defarr =3D NULL; Zoptval v; @@ -1681,6 +1681,14 @@ bin_zparseopts(char *nam, char **args, = UNUSED(Options ops), UNUSED(int func)) } extract =3D 1; break; + case 'F': + if (o[2]) { + args--; + o =3D NULL; + break; + } + fail =3D 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 =3D lookup_opt(o + 1))) { while (*++o) { if (!(d =3D sopts[STOUC(*o)])) { + if (fail) { + zwarnnam(nam, "bad option: %c", *o); + return 1; + } o =3D NULL; break; } diff --git a/Test/V12zparseopts.ztst b/Test/V12zparseopts.ztst new file mode 100644 index 000000000..d7fc33f72 --- /dev/null +++ b/Test/V12zparseopts.ztst @@ -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+=3D( "${(q-)2} ${(q-)${(P)1}[$2]}" ) + done + print -r - ${(j< >)${(@o)_arr}} + } + else + ZTST_unimplemented=3D"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 + } $=3D1 + 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=3D( -x -y -z ) + zparseopts -D -K -a optv - a b: c:- z + print -r - ret: $?, optv: $optv, argv: $argv + } $=3D1 + 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=3D( -b 1 -z '' ) + zparseopts -D -K -A opts - a b: c:- z + print -r - ret: $?, opts: "$( order_assoc opts )", argv: $argv + } $=3D1 + 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:=3D-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=3Daa b:=3Dab 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 + } $=3D1 + # -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 + } $=3D1 + 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 '\+ \: \=3D \\' '+ : =3D \'; do + () { + local -a optv + zparseopts -D -a optv - $=3Dspecs + print -r - ret: $?, optv: $optv, argv: $argv + } -+:=3D\\ 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: -+ -: -=3D -\, argv: 1 2 3 +>ret: 0, optv: -+ -: -=3D -\, 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 - $=3Dspecs + 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