Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

[PATCH] contrib: add zgetopt (again)



i merged this previously (workers/53516) but i realised that the api was
broken, i'd run out of energy to work on it for a while, and i didn't
want to risk it being released in that state. so i reverted it

here's my second attempt

compared to the first it fixes the broken api by removing support for
--alternative, integrates `zparseopts -n` from w/54361, integrates the
empty-spec syntax from w/54362, and makes several trivial and mostly
non-functional changes to formatting, comments, docs, tests, etc that i
realised i wanted after looking at it again

currently i have no plans to re-add --alternative, it's not a major loss

compatible with w/54376 but doesn't require it

dana


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
diff --git a/NEWS b/NEWS
index 952071dbe..f18cb7a76 100644
--- a/NEWS
+++ b/NEWS
@@ -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: '\'\'' "" '




Messages sorted by: Reverse Date, Date, Thread, Author