diff options
author | dana <dana@dana.is> | 2025-04-27 07:58:23 -0500 |
---|---|---|
committer | dana <dana@dana.is> | 2025-04-27 07:58:23 -0500 |
commit | 84ef0c523878625feeed8cd0a5c142929d8b4d06 (patch) | |
tree | 7a15bc37dc6c74496aaf7aec3326f7d2cef222c0 /Functions | |
parent | 2601c191865d3d49e3e57fbb9f6224bcbd22f7a4 (diff) | |
download | zsh-84ef0c523878625feeed8cd0a5c142929d8b4d06.tar.gz zsh-84ef0c523878625feeed8cd0a5c142929d8b4d06.zip |
53516: add zgetopt contrib function
Diffstat (limited to 'Functions')
-rwxr-xr-x | Functions/Misc/zgetopt | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/Functions/Misc/zgetopt b/Functions/Misc/zgetopt new file mode 100755 index 000000000..5fc1e7725 --- /dev/null +++ b/Functions/Misc/zgetopt @@ -0,0 +1,198 @@ +#!/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 + +# Very stupid and brittle internal wrapper around zparseopts used to insert the +# caller name into its error messages, allowing us to implement --name. This +# MUST be called with -v, since argv has the options to zparseopts itself +__zgetopt_zparseopts() { + local __err __ret + + __err=$( zparseopts "$@" 2>&1 ) + __ret=$? + + zparseopts "$@" &> /dev/null && return + + # Raw error message should look like this: + # zgetopt_zparseopts:zparseopts:3: bad option: -x + [[ -n $__err ]] && print -ru2 - ${__err/#*:zparseopts:<->:/$name:} + return __ret +} + +local optspec pat i posix=0 +local -a match mbegin mend optvv argvv +local -a array alt 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 + +# This 0=... makes any error message we get here look a little nicer when we're +# called as a script. Unfortunately the function name overrides $0 in +# zwarnnam() in other scenarios, so this can't be used to implement --name +0=${0:t} zparseopts -D -F -G - \ + {A,-array}:-=array \ + {a,-alternative}=alt \ + {l,-longoptions,-long-options}+:-=lopts \ + {n,-name}:-=name \ + {o,-options}:-=sopts \ +|| { + print -ru2 "usage: ${0:t} [-A <array>] [-a] [-l <spec>] [-n <name>] [-o <spec>] -- <args>" + return 2 +} + +# Default to the caller's name +(( $#name )) && name=( "${(@)name/#(-n|--name=)/}" ) +[[ -n $name ]] || name=( ${funcstack[2]:-${ZSH_ARGZERO:t}} ) + +(( $#array )) && array=( "${(@)array/#(-A|--array=)/}" ) + +if [[ $ZSH_EVAL_CONTEXT != toplevel ]]; then + [[ $array == *[^A-Za-z0-9_.]* ]] && { + print -ru2 - "${0:t}: invalid array name: $array" + return 2 + } + (( $#array )) || array=( argv ) + +elif [[ -n $array ]]; then + print -ru2 - "${0:t}: -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 - "${0:t}: 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 - "${0:t}: 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 - "${0:t}: 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 - "${0:t}: invalid long-option spec: ${${(@M)lopts:#$~pat}[1]}" + return 2 +} + +(( $#alt )) || 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]} ) + +# POSIXLY_CORRECT: Stop parsing options after first non-option argument +if (( posix )); then + tmp=( "$@" ) + __zgetopt_zparseopts -D -F -G -a optvv -v tmp - $specs || return 1 + argvv=( "${(@)tmp}" ) + +# Default: Permute options following non-option arguments +else + tmp=( "$@" ) + __zgetopt_zparseopts -D -E -F -G -a optvv -v tmp - $specs || return 1 + argv=( "${(@)tmp}" ) + # -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. This can't be perfectly +# accurate if Sun-style (-a) long options are used with optional optargs -- e.g. +# if you have specs a:: and abc::, then argument -abc=d is ambiguous. We don't +# guarantee which one is prioritised +(( $#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 - "${0:t}: parse error, please report!" + print -ru2 - "${0:t}: specs: ${(j< >)${(@q+)specs}}" + print -ru2 - "${0:t}: old_optvv: ${(j< >)${(@q+)old_optvv}}" + print -ru2 - "${0:t}: cur: $cur" + optvv+=( $cur ) # I guess? + fi + done +} + +if [[ -n $array ]]; then + # Use EXIT trap to assign in caller's context + trap "$array=( ${(j< >)${(@q+)optvv}} -- ${(j< >)${(@q+)argvv}} )" EXIT + +elif [[ $ZSH_EVAL_CONTEXT != toplevel ]]; then + print -r - "${(@q+)optvv}" -- "${(@q+)argvv}" + +# If called as a script, 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 |