[PATCH] contrib: add zgetopt (again)

dana <[email protected]>
Newsgroups gmane.comp.shells.zsh.devel
Message-ID <[email protected]>
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: '\'\'' "" '
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.