summaryrefslogtreecommitdiff
path: root/Functions/Misc/zgetopt
blob: 5fc1e77250c6c393ab475b577b0897ebe3b3554b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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