-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbash-args.sh
More file actions
287 lines (243 loc) · 11.1 KB
/
bash-args.sh
File metadata and controls
287 lines (243 loc) · 11.1 KB
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
# @name flag
# @type function
# @desc Parse a boolean flag (e.g., -v, --verbose)
#
# @usage args:flag [-r|--required] [-b|--bundle] [--count] <long> <short> [--err <msg>]
#
# @flag [-r|--required] -- Make the flag required.
# @flag [-b|--bundle] -- Also match within combined short flags (e.g., -vfs). Requires <short>.
# @flag [--count] -- Count occurrences instead of setting a boolean. Variable will be set to the number of matches (0 if absent).
# @arg <long> -- Long name of the flag (also used as variable name).
# @arg <short> -- Short (1 letter) name of the flag. Use "" for long-only.
# @opt [--err <msg>] -- Error message printed to stderr on failure.
#
# @return 0 -- Flag found or optional and absent.
# @return 1 -- Flag absent and required.
function args:flag() {
declare -p ARGS &>/dev/null || { echo "ERROR: Add 'local ARGS=(\"\$@\")' before ${FUNCNAME[0]}." >&2; return 1; };
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS"
local err=""; (($# >= 2)) && [[ "${*: -2:1}" == "--err" ]] && err="${*: -1}" && set -- "${@:1:$#-2}";
local required="false"; [[ "${1:-}" == "-r" || "${1:-}" == "--required" ]] && required="true" && shift;
local bundle="false"; [[ "${1:-}" == "-b" || "${1:-}" == "--bundle" ]] && bundle="true" && shift;
local count="false"; [[ "${1:-}" == "--count" ]] && count="true" && shift;
local -n __value_="${1//[^_0-9a-zA-Z]/_}";
local long="${1?ERROR: args:flag requires <long>}"; shift;
local short="${1?ERROR: args:flag requires <short>}"; shift;
local scan; [[ -z "$short" ]] && scan="^--${long}$" || scan="^-${short}$|^--${long}$";
local hit hits=0;
while args::capture hit "$scan"; do ((++hits)); done
# Bundle: also match within combined short flags (-vfs)
if [[ "$bundle" == "true" && -n "$short" ]]; then
local bscan="^-[^-]*${short}";
local rest;
while args::capture hit "$bscan"; do
((++hits));
rest="${hit//$short/}"; rest="${rest#-}";
TOKENS=(${rest:+-$rest} "${TOKENS[@]}");
done
fi
# Flag when --count value=$hits otherwise value=true
((hits > 0)) && __value_="true";
[[ "$count" == "true" ]] && __value_="$hits";
# Flag found or optional and absent.
((hits > 0)) || [[ "$required" == "false" ]] && return 0;
# Flag absent and required, show error
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
}
# @name opt
# @type function
# @desc Parse an option with a value (e.g., -n foo, --name=foo)
#
# @usage args:opt [-r|--required] [-a|--accumulate] <long> <short> [pattern] [--err <msg>]
#
# @flag [-r|--required] -- Make the option required.
# @flag [-a|--accumulate] -- Collect all occurrences into an array instead of last-wins. Variable must be declared as an array.
# @arg <long> -- Long name of the option (also used as variable name).
# @arg <short> -- Short (1 letter) name of the option. Use "" for long-only.
# @arg [pattern] -- RegEx pattern to validate value. Default "(.*)".
# @opt [--err <msg>] -- Error message printed to stderr on failure.
#
# @return 0 -- Option found and matches, or absent and optional.
# @return 1 -- Option absent and required, or found and mismatched.
function args:opt() {
declare -p ARGS &>/dev/null || { echo "ERROR: Add 'local ARGS=(\"\$@\")' before ${FUNCNAME[0]}." >&2; return 1; };
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS"
local err=""; (($# >= 2)) && [[ "${*: -2:1}" == "--err" ]] && err="${*: -1}" && set -- "${@:1:$#-2}";
local required="false"; [[ "${1:-}" == "-r" || "${1:-}" == "--required" ]] && required="true" && shift;
local accumulate="false"; [[ "${1:-}" == "-a" || "${1:-}" == "--accumulate" ]] && accumulate="true" && shift;
local -n __value_="${1?ERROR: args:opt requires <long>}";
local long="${1}"; shift;
local short="${1?ERROR: args:opt requires <short>}"; shift;
local pattern="${1:-(.*)}"; shift || true;
local scan="^--${long}$|^--${long}=(.+)$"
[[ -n "$short" ]] && scan="^-${short}$|^-${short}(.+)|--${long}|--${long}=(.+)$";
# Scan all args and accumulate $values
local rc=0 captured values=();
while args::capture captured "$scan" "$pattern" || { rc=$?; false; }; do
values+=("$captured");
done
# Option was found but didn't match $pattern
((rc == 2)) && { [[ -n "$err" ]] && echo "$err" >&2; return 1; };
# Opt when --accumulate value=$values otherwise value=$values[0]
((${#values[@]} > 0)) && __value_="${values[0]}";
[[ "$accumulate" == "true" ]] && __value_=("${values[@]}");
# Option found and matches, or absent and optional.
((${#values[@]} > 0)) || [[ "$required" == "false" ]] && return 0;
# Option absent and required, or found and mismatched.
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
}
# @name arg
# @type function
# @desc Parse a positional argument
#
# @usage args:arg [-o|--optional] <name> [pattern] [--err <msg>]
#
# @flag [-o|--optional] -- Make the argument optional.
# @arg <name> -- Variable name to assign the captured value.
# @arg [pattern] -- RegEx pattern to validate value. Default "(.*)".
# @opt [--err <msg>] -- Error message printed to stderr on failure.
#
# @return 0 -- Argument found and matches, or absent and optional.
# @return 1 -- Argument absent and required, or found and mismatched.
function args:arg() {
declare -p ARGS &>/dev/null || { echo "ERROR: Add 'local ARGS=(\"\$@\")' before ${FUNCNAME[0]}." >&2; return 1; };
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS"
local err=""; (($# >= 2)) && [[ "${*: -2:1}" == "--err" ]] && err="${*: -1}" && set -- "${@:1:$#-2}";
local optional="false"; [[ "${1:-}" == "-o" || "${1:-}" == "--optional" ]] && optional="true" && shift;
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n __value_="${1?ERROR: args:arg requires <name>}"; shift;
local pattern="${1:-(.*)}"; shift || true;
[[ "${TOKENS[0]:-}" == "--" ]] && TOKENS=("${TOKENS[@]:1}");
if ((${#TOKENS[@]} < 1)); then
[[ "$optional" == "true" ]] && return 0;
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
fi
local captured="${TOKENS[0]}";
if [[ ! "$captured" =~ $pattern ]]; then
[[ "$optional" == "true" ]] && return 0;
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
fi
# shellcheck disable=SC2178 # nameref assigned a string
__value_="${BASH_REMATCH[1]:-$captured}";
# rewrite the $TOKENS array without the captured tokens
TOKENS=("${TOKENS[@]:1}");
}
# @name varg
# @type function
# @desc Parse variadic (rest) arguments into an array
#
# @usage args:varg [-o|--optional] <name> [--err <msg>]
#
# @flag [-o|--optional] -- Make the arguments optional.
# @arg <name> -- Array variable name to capture rest values.
# @opt [--err <msg>] -- Error message printed to stderr on failure.
#
# @return 0 -- At least one argument found, or optional and none found.
# @return 1 -- No arguments and required.
function args:varg() {
declare -p ARGS &>/dev/null || { echo "ERROR: Add 'local ARGS=(\"\$@\")' before ${FUNCNAME[0]}." >&2; return 1; };
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS";
local err=""; (($# >= 2)) && [[ "${*: -2:1}" == "--err" ]] && err="${*: -1}" && set -- "${@:1:$#-2}";
local optional="false"; [[ "${1:-}" == "-o" || "${1:-}" == "--optional" ]] && optional="true" && shift;
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n __value_="${1?ERROR: args:varg requires <name>}"; shift;
[[ "${TOKENS[0]:-}" == "--" ]] && TOKENS=("${TOKENS[@]:1}");
if ((${#TOKENS[@]} < 1)); then
[[ "$optional" == "true" ]] && return 0;
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
fi
__value_=("${TOKENS[@]}");
# rewrite the $TOKENS array without the captured tokens
TOKENS=();
}
# @name sub
# @type function
# @desc Parse a subcommand and split arguments at the subcommand boundary
#
# @usage args:sub [-o|--optional] <name> <rest> <pattern> [--err <msg>]
#
# @flag [-o|--optional] -- Make the subcommand optional.
# @arg <name> -- Variable name to assign the subcommand name.
# @arg <rest> -- Array variable name to capture arguments after the subcommand.
# @arg <pattern> -- RegEx pattern to match the subcommand.
# @opt [--err <msg>] -- Error message printed to stderr on failure.
#
# @return 0 -- Subcommand found, or optional and absent.
# @return 1 -- Subcommand absent and required, or found and mismatched.
function args:sub() {
declare -p ARGS &>/dev/null || { echo "ERROR: Add 'local ARGS=(\"\$@\")' before ${FUNCNAME[0]}." >&2; return 1; };
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS";
local err=""; (($# >= 2)) && [[ "${*: -2:1}" == "--err" ]] && err="${*: -1}" && set -- "${@:1:$#-2}";
local optional="false"; [[ "${1:-}" == "-o" || "${1:-}" == "--optional" ]] && optional="true" && shift;
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n __value_="${1?ERROR: args:sub requires <name>}"; shift;
local -n __rest_="${1?ERROR: args:sub requires <rest>}"; shift;
local pattern="${1?ERROR: args:sub requires <pattern>}"; shift;
local i captured;
for ((i=0; i<${#TOKENS[@]}; i++)); do
captured="${TOKENS[i]}";
[[ "$captured" == "--" ]] && break;
[[ ! "$captured" =~ $pattern ]] && continue;
# shellcheck disable=SC2178 # nameref assigned a string
__value_="${BASH_REMATCH[1]:-$captured}";
__rest_=("${TOKENS[@]:i+1}");
TOKENS=("${TOKENS[@]:0:i}");
return 0;
done
[[ "$optional" == "true" ]] && return 0;
[[ -n "$err" ]] && echo "$err" >&2;
return 1;
}
# @name capture
# @type internal
# @desc Scan TOKENS for a matching token, extract value, remove from TOKENS. Stops at --.
#
# @usage args::capture <ref> <token> [value]
#
# @arg <ref> -- Reference to assign captured value.
# @arg <token> -- Token structure.
# @arg [pattern] -- Value must match this pattern.
#
# @return 0 -- Token found and captured.
# @return 1 -- No matching token found.
# @return 2 -- Token found and consumed, value doesn't match pattern.
function args::capture() {
# shellcheck disable=SC2178 # nameref is a string, points to an array
local -n TOKENS="ARGS";
local -n __ref_="${1}"; shift;
local tok="${1}"; shift;
local pat="${1:-}"; shift
local i token tokens=() value;
for ((i=0; i<${#TOKENS[@]}; i++)); do
token="${TOKENS[i]}";
# Stop at positional arguments separator
[[ "$token" == "--" ]] && break;
# Token not matched, go to next token (but save all uncaptured tokens)
[[ ! "$token" =~ $tok ]] && tokens+=("$token") && continue;
# Flag matched (no value pattern provided)
# shellcheck disable=SC2034 # ref is a nameref, assigned for the caller
[[ -z "$pat" ]] && __ref_="$token" && TOKENS=("${tokens[@]}" "${TOKENS[@]:i+1}") && return 0;
# Value attached to option -o123 or --opt=123
value="${BASH_REMATCH[1]:-${BASH_REMATCH[2]:-}}";
# Value deattached from option -o 123 or --opt 123
[[ -z "$value" ]] && ((++i)) && value="${TOKENS[i]}";
# Value must match this pattern.
# shellcheck disable=SC2034 # ref is a nameref, assigned for the caller
[[ "$value" =~ $pat ]] && __ref_="${BASH_REMATCH[1]:-$value}" && TOKENS=("${tokens[@]}" "${TOKENS[@]:i+1}") && return 0;
# Value found but didn't match pattern.
TOKENS=("${tokens[@]}" "${TOKENS[@]:i+1}");
return 2;
done
return 1;
}