diff options
| author | dana <dana@dana.is> | 2026-04-29 23:13:01 -0500 |
|---|---|---|
| committer | dana <dana@dana.is> | 2026-04-29 23:13:16 -0500 |
| commit | a9aa972cf44e49c8ffde6517677e75fe8fceea34 (patch) | |
| tree | 0d75cd9516d5b1450551fc74a4c6904bc1136551 | |
| parent | unposted: completion: add _compinit (diff) | |
| download | zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar.gz zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar.bz2 zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar.lz zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar.xz zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.tar.zst zsh-a9aa972cf44e49c8ffde6517677e75fe8fceea34.zip | |
54395: contrib: add zgetopt (again)
| -rw-r--r-- | ChangeLog | 3 | ||||
| -rw-r--r-- | Doc/Zsh/contrib.yo | 73 | ||||
| -rwxr-xr-x | Functions/Misc/zgetopt | 178 | ||||
| -rw-r--r-- | NEWS | 3 | ||||
| -rw-r--r-- | Test/Z04zgetopt.ztst | 196 |
5 files changed, 453 insertions, 0 deletions
@@ -1,5 +1,8 @@ 2026-04-29 dana <dana@dana.is> + * 54395: Doc/Zsh/contrib.yo, Functions/Misc/zgetopt, NEWS, + Test/Z04zgetopt.ztst: contrib: add zgetopt (again) + * unposted: Completion/Zsh/Command/_compinit: add _compinit 2026-04-28 dana <dana@dana.is> diff --git a/Doc/Zsh/contrib.yo b/Doc/Zsh/contrib.yo index 03572bf45..1e088907c 100644 --- a/Doc/Zsh/contrib.yo +++ b/Doc/Zsh/contrib.yo @@ -4651,6 +4651,79 @@ Same as tt(zmv -C) and tt(zmv -L), respectively. These functions do not appear in the zsh distribution, but can be created by linking tt(zmv) to the names tt(zcp) and tt(zln) in some directory in your tt(fpath). ) +findex(zgetopt) +item(tt(zgetopt) [ tt(-A) var(array) ] [ tt(-l) var(spec) ] [ tt(-n) var(name) ] [ tt(-o) var(spec) ] tt(--) [ var(arg) ... ])( +This is a wrapper around tt(zparseopts) (from tt(zsh/zutil)) which +provides an interface similar to the util-linux implementation of +tt(getopt+LPAR()1+RPAR()) (sometimes called `GNU tt(getopt)'). It +simplifies GNU-style argument parsing (including permutation) and +can make it easier to write functions and scripts with complex APIs, +particularly ones where the order of options is significant. + +The typical usage pattern is as follows: + +example(zgetopt -o abc: -l aaa,bbb,ccc: -- "$@" || return +while (( $# )); do + case $1 in + -a|--aaa+RPAR() ...; shift ;; # handle -a + -b|--bbb+RPAR() ...; shift ;; # handle -b + -c|--ccc+RPAR() ...; shift 2 ;; # handle -c and arg + --+RPAR() ...; shift; break ;; # end of options + esac +done +# handle operands) + +It can also be called as a stand-alone script from other shells +using the more traditional print-and-eval pattern: + +example(args=$( zgetopt -n myscript -o abc: -l aaa,bbb,ccc: -- "$@" ) || return +eval set -- "$args" +while [ $# -ne 0 ]; do ...; done) + +Options: + +startsitem() +sitem(tt(-A var(array)))(When called as a function, assign the parsed +arguments to the named array var(array). Defaults to tt(argv), which +overwrites the caller's positional parameters. Has no meaning when +called as a script, in which case the parsed and quoted arguments are +always printed to standard output. An empty string forces the +printing behaviour in either mode.) +sitem(tt(-l var(spec)))(Specify long options to recognise when +parsing. These should be given using just the option name (no +dashes), suffixed by `tt(:)' or `tt(::)' if it takes a mandatory or +optional argument respectively. Multiple options can be defined +either by separating them by commas or by supplying -l again. +Example: tt(-l foo,bar: -l baz)) +sitem(tt(-n var(name)))(Specify the name to use in the error message +if argument parsing fails. Defaults to the name of the nearest +calling function or the base name of tt($ZSH_ARGZERO). Note that +errors related to the usage of tt(zgetopt) itself are always reported +as coming from tt(zgetopt).) +sitem(tt(-o var(spec)))(Specify short options to recognise when +parsing. These should be given as a single string, in the same format +used by the tt(getopts) built-in or the tt(getopt+LPAR()3+RPAR()) +library function, again using `tt(:)' or `tt(::)' to indicate a +mandatory or optional argument. The spec may be prefixed with `tt(+)' +to indicate that option parsing should stop at the first non-option +argument (equivalent to setting the environment variable +tt(POSIXLY_CORRECT)). Example: tt(-o ab:cd::)) +endsitem() + +At least one of tt(-o) or tt(-l) must be given. The function's own +options should be followed by zero or more arguments to parse. It is +critical that these be separated explicitly by `tt(--)', as in the +above examples, to ensure that the function can accurately +distinguish the arguments it's meant to parse from its own. + +Refer to the manual for util-linux's tt(getopt+LPAR()1+RPAR()) for +more information about the way arguments are parsed and results are +returned. Note however that this function is not intended to be a +complete re-implementation. In particular: it omits all +portability/compatibility features, it doesn't support the +tt(--alternative) option, and it doesn't support abbreviating long +options. +) item(tt(zkbd))( See subref(Keyboard Definition)(above). ) diff --git a/Functions/Misc/zgetopt b/Functions/Misc/zgetopt new file mode 100755 index 000000000..718381cbd --- /dev/null +++ b/Functions/Misc/zgetopt @@ -0,0 +1,178 @@ +#!/bin/zsh -f + +# wrapper around zparseopts which gives it an interface similar to util-linux's +# getopt(1). see zshcontrib(1) for documentation + +emulate -L zsh -o extended_glob +zmodload -i zsh/zutil || return 3 + +local caller=${funcstack[2]:-${ZSH_ARGZERO:t}} +local errname=$caller:${0:t} +local optspec pat i posix=0 +local -a match mbegin mend optvv argvv +local -a array lopts sopts name +local -a specs no_arg_opts req_arg_opts opt_arg_opts tmp + +# same as leading + in short-opts spec +(( $+POSIXLY_CORRECT )) && posix=1 + +zparseopts -n $errname -D -F -G - \ + {A,-array}:-=array \ + {l,-longoptions,-long-options}+:-=lopts \ + {n,-name}:-=name \ + {o,-options}:-=sopts \ +|| { + print -ru2 "usage: ${0:t} [-A <array>] [-l <spec>] [-n <name>] [-o <spec>] -- <arg> ..." + return 2 +} + +name=( ${(@)name/#(-n|--name=)/} ) +[[ -n $name ]] || name=( $caller ) + +(( $#array )) && array=( "${(@)array/#(-A|--array=)/}" ) + +if [[ $zsh_eval_context[-1] == shfunc ]]; then + [[ $array == *[^A-Za-z0-9_.]* ]] && { + print -ru2 - "$errname: invalid array name: $array" + return 2 + } + (( $#array )) || array=( argv ) + +elif [[ -n $array ]]; then + print -ru2 - "$errname: -A option not meaningful unless called as function" + return 2 +fi + +# getopt requires a short option spec; we'll require either short or long +(( $#sopts || $#lopts )) || { + print -ru2 - "$errname: missing option spec" + return 2 +} + +optspec=${(@)sopts/#(-o|--options=)/} +sopts=( ) + +for (( i = 1; i <= $#optspec; i++ )); do + # leading '+': act POSIXLY_CORRECT + if [[ $i == 1 && $optspec[i] == + ]]; then + posix=1 + # leading '-': should leave operands interspersed with options, but this is + # not really possible with zparseopts + elif [[ $i == 1 && $optspec[i] == - ]]; then + print -ru2 - "$errname: optspec with leading - (disable operand collection) not supported" + return 2 + # special characters: [+=\\] because they're special to zparseopts, ':' + # because it's special to getopt, '-' because it's the parsing terminator + elif [[ $optspec[i] == [+:=\\-] ]]; then + print -ru2 - "$errname: invalid short-option name: $optspec[i]" + return 2 + # 'a' + elif [[ $optspec[i+1] != : ]]; then + sopts+=( $optspec[i] ) + # 'a:' + elif [[ $optspec[i+2] != : ]]; then + sopts+=( $optspec[i]: ) + (( i += 1 )) + # 'a::' + elif [[ $optspec[i+3] != : ]]; then + sopts+=( $optspec[i]:: ) + (( i += 2 )) + fi +done + +lopts=( ${(@)lopts/#(-l|--long(|-)options=)/} ) +lopts=( ${(@s<,>)lopts} ) + +# don't allow characters that are special to zparseopts in long-option specs. +# see above +pat='(*[+=\\]*|:*|*:::##|*:[^:]*)' +[[ -n ${(@M)lopts:#$~pat} ]] && { + print -ru2 - "$errname: invalid long-option spec: ${${(@M)lopts:#$~pat}[1]}" + return 2 +} + +lopts=( ${(@)lopts/#/-} ) +specs=( $sopts $lopts ) + +# used below to identify options with optional optargs +no_arg_opts=( ${(@)${(@M)specs:#*[^:]}/#/-} ) +req_arg_opts=( ${(@)${(@)${(@M)specs:#*[^:]:}/#/-}/%:#} ) +opt_arg_opts=( ${(@)${(@)${(@M)specs:#*::}/#/-}/%:#} ) + +# getopt returns all instances of each option given, so add + +specs=( ${(@)specs/%(#b)(:#)/+$match[1]} ) + +# if we've got nothing now, the user probably gave -o '' or sth. pass an +# explicit '' to zparseopts +(( $#specs )) || specs=( '' ) + +# POSIXLY_CORRECT: stop parsing options after first non-option argument +if (( posix )); then + argvv=( "$@" ) + zparseopts -n $name -a optvv -v argvv -D -F -G - "${(@)specs}" || return 1 + +# default: permute options following non-option arguments +else + zparseopts -n $name -a optvv -D -E -F -G - "${(@)specs}" || return 1 + # -D + -E leaves an explicit -- in argv where-ever it might appear + local seen + while (( $# )); do + [[ -z $seen && $1 == -- ]] && seen=1 && shift && continue + argvv+=( "$1" ) + shift + done +fi + +# getopt outputs all optargs as separate parameters, even missing optional ones, +# so we scan through and add/separate those if needed +(( $#opt_arg_opts )) && { + local cur next + local -a old_optvv=( "${(@)optvv}" ) + optvv=( ) + + for (( i = 1; i <= $#old_optvv; i++ )); do + cur=$old_optvv[i] + next=$old_optvv[i+1] + # option with no optarg + if [[ -n ${no_arg_opts[(r)$cur]} ]]; then + optvv+=( $cur ) + # option with required optarg -- will appear in next element + elif [[ -n ${req_arg_opts[(r)$cur]} ]]; then + optvv+=( $cur "$next" ) + (( i++ )) + # long option with optional optarg -- will appear in same element delimited + # by = (even if missing) + elif [[ $cur == *=* && -n ${opt_arg_opts[(r)${cur%%=*}]} ]]; then + optvv+=( ${cur%%=*} "${cur#*=}" ) + # short option with optional optarg -- will appear in same element with no + # delimiter (thus the option appears alone if the optarg is missing) + elif [[ -n ${opt_arg_opts[(r)${(M)cur#-?}]} ]]; then + optvv+=( ${(M)cur#-?} "${cur#-?}" ) + # ??? + else + print -ru2 - "$errname: parse error, please report!" + print -ru2 - "$errname: specs: ${(j< >)${(@q+)specs}}" + print -ru2 - "$errname: old_optvv: ${(j< >)${(@q+)old_optvv}}" + print -ru2 - "$errname: cur: $cur" + optvv+=( $cur ) # i guess? + fi + done +} + +# called as function, assign to array. use EXIT trap to assign in caller's +# context +if [[ -n $array ]]; then + trap "$array=( ${(j< >)${(@q+)optvv}} -- ${(j< >)${(@q+)argvv}} )" EXIT + +# called as function, print +elif [[ $zsh_eval_context[-1] == shfunc ]]; then + print -r - "${(@q+)optvv}" -- "${(@q+)argvv}" + +# called as a script, print. use unconditional single-quoting. this is ugly but +# it's the closest to what getopt does and it offers compatibility with legacy +# shells +else + print -r - "${(@qq)optvv}" -- "${(@qq)argvv}" +fi + +return 0 @@ -66,6 +66,9 @@ The zsh/zutil module's zparseopts builtin learnt the following: - a single empty option spec signifies that no options are recognised +A new contrib function zgetopt was added. It wraps `zparseopts -G` to +provide an interface similar to util-linux's getopt(1). + The module zsh/pcre has been updated to use the pcre2 library. The new zsh/random module defines an SRANDOM parameter, zrand_float() diff --git a/Test/Z04zgetopt.ztst b/Test/Z04zgetopt.ztst new file mode 100644 index 000000000..f1a14d329 --- /dev/null +++ b/Test/Z04zgetopt.ztst @@ -0,0 +1,196 @@ +%prep + + autoload -Uz zgetopt + +%test + + zgetopt -A '' -- a b c + zgetopt -A '' -o '' -- a b c + zgetopt -A '' -l '' -- a b c +0:-o or -l required +?(eval):zgetopt: missing option spec +>-- a b c +>-- a b c + + zgetopt -A '' -o - -- a b c + zgetopt -A '' -o -a -- a b c + zgetopt -A '' -o a- -- a b c + zgetopt -A '' -o a+ -- a b c + zgetopt -A '' -o a= -- a b c + zgetopt -A '' -o a\\ -- a b c + zgetopt -A '' -o :a -- a b c + zgetopt -A '' -o a::: -- a b c + zgetopt -A '' -o '' -- a b c + zgetopt -A '' -o + -- a b c +0:weird short-option specs +?(eval):zgetopt: optspec with leading - (disable operand collection) not supported +?(eval):zgetopt: optspec with leading - (disable operand collection) not supported +?(eval):zgetopt: invalid short-option name: - +?(eval):zgetopt: invalid short-option name: + +?(eval):zgetopt: invalid short-option name: = +?(eval):zgetopt: invalid short-option name: \ +?(eval):zgetopt: invalid short-option name: : +?(eval):zgetopt: invalid short-option name: : +>-- a b c +>-- a b c + + zgetopt -A '' -l a,+ -- a b c + zgetopt -A '' -l a,= -- a b c + zgetopt -A '' -l a,\\ -- a b c + zgetopt -A '' -l a,: -- a b c + zgetopt -A '' -l a,:b -- a b c + zgetopt -A '' -l a,b:b -- a b c + zgetopt -A '' -l a,b::: -- a b c + zgetopt -A '' -l '' -- a b c + zgetopt -A '' -l , -- a b c + zgetopt -A '' -l a,,,,,b -- a b c + zgetopt -A '' -l - -- a b c --- +0:weird long-option specs +?(eval):zgetopt: invalid long-option spec: + +?(eval):zgetopt: invalid long-option spec: = +?(eval):zgetopt: invalid long-option spec: \ +?(eval):zgetopt: invalid long-option spec: : +?(eval):zgetopt: invalid long-option spec: :b +?(eval):zgetopt: invalid long-option spec: b:b +?(eval):zgetopt: invalid long-option spec: b::: +>-- a b c +>-- a b c +>-- a b c +>--- -- a b c + + zgetopt -A '' -o ab:c:: -- a b c + zgetopt -A '' -o ab:c:: -- -a + zgetopt -A '' -o ab:c:: -- -a a b c + zgetopt -A '' -o ab:c:: -- -a a -b c + zgetopt -A '' -o ab:c:: -- -a a -b -c + zgetopt -A '' -o ab:c:: -- -a a -b -c d + zgetopt -A '' -o ab:c:: -- -a a -b -c -c + zgetopt -A '' -o ab:c:: -- -a a -b -c -c d + zgetopt -A '' -o ab:c:: -- -a a -b -c -cd +0:short options +>-- a b c +>-a -- +>-a -- a b c +>-a -b c -- a +>-a -b -c -- a +>-a -b -c -- a d +>-a -b -c -c '' -- a +>-a -b -c -c '' -- a d +>-a -b -c -c d -- a + + zgetopt -A '' -l aaa,bbb:,ccc:: -- a b c + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a b c + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb c + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb=c + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc d + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc --ccc + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc --ccc d + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc --ccc= d + zgetopt -A '' -l aaa,bbb:,ccc:: -- --aaa a --bbb --ccc --ccc=d +0:long options +>-- a b c +>--aaa -- +>--aaa -- a b c +>--aaa --bbb c -- a +>--aaa --bbb c -- a +>--aaa --bbb --ccc -- a +>--aaa --bbb --ccc -- a d +>--aaa --bbb --ccc --ccc '' -- a +>--aaa --bbb --ccc --ccc '' -- a d +>--aaa --bbb --ccc --ccc '' -- a d +>--aaa --bbb --ccc --ccc d -- a + + zgetopt -A '' -o '' +0:zero args to parse +>-- + + zgetopt -A '' -o '' -- -- a b c + zgetopt -A '' -o '' -- a b -- c + zgetopt -A '' -o '' -- a b c -- + zgetopt -A '' -o c -- a b -- -c + zgetopt -A '' -o c -- a b - -c +0:parsing terminator +>-- a b c +>-- a b c +>-- a b c +>-- a b -c +>-c -- a b - + + zgetopt -A '' -o a -- a -a b + zgetopt -A '' -o +a -- a -a b + POSIXLY_CORRECT=1 zgetopt -A '' -o a -- a -a b +0:POSIXLY_CORRECT +>-a -- a b +>-- a -a b +>-- a -a b + + zgetopt -A '' -o '' -- 'foo bar' $'bar\tbaz' $'\a\'\a' +0:function-mode quoting style +>-- 'foo bar' $'bar\tbaz' $'\C-G\'\C-G' + + zgetopt -A '' -o '' -- a -a b + zgetopt -A '' -o '' -- a --a b +1:bad options +?(eval): bad option: -a +?(eval): bad option: --a + + zgetopt -A '' -o a: -- a -a + zgetopt -A '' -l a: -- a --a +1:missing optargs +?(eval): missing argument for option: -a +?(eval): missing argument for option: --a + + zgetopt -A '' ; echo $? # missing spec + zgetopt -A '' -o '' -x ; echo $? # bad option to zgetopt + zgetopt -A '' -o '' -- -y; echo $? # bad option to parse +-:return status +*?\(eval\):zgetopt: missing option spec +*>2 +*?\(eval\):zgetopt: bad option: -x +*?usage:* +*>2 +*?\(eval\): bad option: -y +*>1 + + () { zgetopt -o a -- "$@"; typeset -p argv } -a b c + () { local -a v; zgetopt -A v -o a -- "$@"; typeset -p argv v } -a b c +0:array output +>typeset -g -a argv=( -a -- b c ) +>typeset -g -a argv=( -a b c ) +>typeset -a v=( -a -- b c ) + + zgetopt -A '' -o a: -- -x + zgetopt -A '' -o a: -- -a + () { zgetopt -A '' -o a: -- "$@"; : } -x + func() { zgetopt -A '' -o a: -- "$@"; : }; func -x + f1() { zgetopt -A '' -o a: -- "$@"; : }; f2() { f1 "$@" }; f2 -x +0:automatic name +?(eval): bad option: -x +?(eval): missing argument for option: -a +?(anon): bad option: -x +?func: bad option: -x +?f1: bad option: -x + + zgetopt -n aaa -A '' -o a: -- -x + zgetopt -n aaa -A '' -o a: -- -a + () { zgetopt -n bbb -A '' -o a: -- "$@"; : } -x + func() { zgetopt -n ccc -A '' -o a: -- "$@"; : }; func -x + f1() { zgetopt -n ddd -A '' -o a: -- "$@"; : }; f2() { f1 "$@" }; f2 -x +0:manual name with -n +?aaa: bad option: -x +?aaa: missing argument for option: -a +?bbb: bad option: -x +?ccc: bad option: -x +?ddd: bad option: -x + + () { + zgetopt -A '' -n example.bash -o ab:c:: -l a-long,b-long:,c-long:: -- "$@" + } \ + -a --a-long \ + -barg_bs1 -b arg_bs2 --b-long=arg_bl1 --b-long arg_bl2 \ + -carg_cs1 -c not_arg_cs1 --c-long=arg_cl1 --c-long not_arg_cl2 \ + arg_p "string with quotes and space: '' \"\" " +0:example from util-linux's getopt-example.bash +>-a --a-long -b arg_bs1 -b arg_bs2 --b-long arg_bl1 --b-long arg_bl2 -c arg_cs1 -c '' --c-long arg_cl1 --c-long '' -- not_arg_cs1 not_arg_cl2 arg_p 'string with quotes and space: '\'\'' "" ' |
