Skip to content

Commit f039c11

Browse files
authored
--enum syntax (#28)
The main thrust of this PR is changing the syntax accepted by `--enum` to be like multi-value options as one can _create_ using this system. Everything else was in service of that goal. A follow-on PR will clean things up after all (client) use sites are updated.
2 parents 7789e79 + 993ea2f commit f039c11

File tree

36 files changed

+1044
-146
lines changed

36 files changed

+1044
-146
lines changed

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,23 @@ versioning principles. Unstable releases do not.
77
### [Unreleased]
88

99
Breaking changes:
10-
* None
10+
* `bashy-core`:
11+
* `arg-processor`:
12+
* Tightened up syntax for passing multi-value arguments.
13+
* Renamed `--eval=` to `--eval[]=`, to be the same as how options defined by
14+
the system work.
1115

1216
Other notable changes:
13-
* None
17+
* `bashy-core`:
18+
* `arg-processor`:
19+
* `--filter` now supports `{...}` to specify a code snippet, just like
20+
`--call` already does.
21+
* `--eval` values can be specified using the same multi-value syntax used by
22+
the rest of the system.
23+
* `misc`:
24+
* Made `vals` more conservative in its output.
25+
* New function `set-array-from-vals`, which is (approximately) the reverse
26+
action of `vals`.
1427

1528
### v2.6 -- 2023-10-24
1629

doc/arg-processor.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ alphanumerics and more dashes, e.g. `--some-option`. In addition:
2424

2525
* Multiple values can be passed to an option by following the option name with
2626
a pair of square brackets and an equal sign (`[]=`) and then a series of
27-
space-separated words, with standard shell rules for quoting and escaping in
28-
order to pass special characters, e.g. `--some-option[]='this "and that"'`.
27+
space-separated words, with quotes recognized as delimiting words with
28+
special characters. Single-quoted (`'...'`) and dollar-quoted (`$'...'`)
29+
strings are interpreted in the shell-usual ways. Double-quoted (`"..."`)
30+
strings are treated like single-quoted; notably, they do not undergo any shell
31+
substitutions or backslash interpretation. E.g. `--some-option[]='this
32+
"and that" $'and other' 'things'`.
2933

3034
This multi-value form will also work for options that don't allow values or
3135
allow only one value (though there are probably few reasons to favor this form
3236
in those cases).
3337

3438
The helper function `vals` is a convenient way to safely pass multiple values
3539
without having to worry about quoting hygiene. (That is, the helper handles it
36-
for you.) For example, `--some-option[]="$(vals "${myArray[@]}")"`.
40+
for you.) For example, `--some-option[]="$(vals -- "${myArray[@]}")"`.
3741

3842
Special cases:
3943
* A single dash (`-`) is interpreted as a non-option argument.
@@ -61,7 +65,7 @@ my-cmd -34 # One positional argument.
6165
my-cmd -- --foo # One positional argument, literally `--foo`.
6266

6367
# Passing arbitrary strings safely to a multi-value option.
64-
my-cmd --strings[]="$(vals "${paths[@]}")"
68+
my-cmd --strings[]="$(vals -- "${paths[@]}")"
6569
```
6670

6771
## Declaring options

scripts/lib/bashy-basics/_setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ function usual-json-output-args {
285285
# Output style.
286286
if (( doOutput )); then
287287
opt-value --var=_bashy_jsonOutputStyle --default=json \
288-
--enum='array json lines none raw raw0' output
288+
--enum[]='array json lines none raw raw0' output
289289
else
290290
_bashy_jsonOutputStyle=json
291291
fi

scripts/lib/bashy-basics/jarray

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ define-usage --with-help $'
2525
'
2626

2727
# Output style.
28-
opt-value --var=outputStyle --default=json --enum='compact json' output
28+
opt-value --var=outputStyle --default=json --enum[]='compact json' output
2929

3030
# Input style.
31-
opt-value --var=inputStyle --default=json --enum='json strings' input
31+
opt-value --var=inputStyle --default=json --enum[]='json strings' input
3232

3333
# The array elements.
3434
rest-arg --var=values values

scripts/lib/bashy-basics/jget

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ opt-value --var=filePath file
3131

3232
# Output style.
3333
opt-value --var=outputStyle --default=json \
34-
--enum='compact json lines none raw raw:slurp raw0 raw0:slurp' output
34+
--enum[]='compact json lines none raw raw:slurp raw0 raw0:slurp' output
3535

3636
# Value to operate on. (Will actually be the first expression if `--file` is
3737
# used.)

scripts/lib/bashy-basics/jval

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ define-usage --with-help $'
6060

6161
# Input style.
6262
opt-value --var=inputStyle --default=none \
63-
--enum='none raw raw:slurp raw0 raw0:slurp read slurp' \
63+
--enum[]='none raw raw:slurp raw0 raw0:slurp read slurp' \
6464
input
6565

6666
# Output style.
6767
opt-value --var=outputStyle --default=json \
68-
--enum='compact json lines none raw raw0' output
68+
--enum[]='compact json lines none raw raw0' output
6969

7070
# List of variable assignments, as parallel arrays of type, name, and value.
7171
varTypes=()

scripts/lib/bashy-basics/timey/print

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ define-usage --with-help $'
2727
'
2828

2929
# Input type.
30-
opt-value --var=inputType --default='secs' --enum='secs rfc822' input
30+
opt-value --var=inputType --default='secs' --enum[]='secs rfc822' input
3131

3232
# UTC?
3333
opt-toggle --var=utc utc

scripts/lib/bashy-basics/timey/secs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ define-usage --with-help $'
2323
'
2424

2525
# Input type.
26-
opt-value --var=inputType --default='secs' --enum='secs rfc822' input
26+
opt-value --var=inputType --default='secs' --enum[]='secs rfc822' input
2727

2828
# Time value to parse.
2929
positional-arg --required --var=time time

scripts/lib/bashy-core/arg-processor.sh

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@
2929
#
3030
# Value-accepting argument definers allow these additional options to pre-filter
3131
# a value, before it gets set or passed to a `--call`:
32-
# * `--filter=<name>` -- Calls the named function passing it a single argument
33-
# value; the function must output a replacement value. If the call fails, the
34-
# argument is rejected. Note: The filter function runs in a subshell, and as
35-
# such it cannot be used to affect the global environment of the main script.
32+
# * `--filter=<name>` or `filter={code}` -- Calls the named function passing it
33+
# a single argument value, or runs the indicated code snippet. The function
34+
# or snippet must output a replacement value. If the call fails, the argument
35+
# is rejected. Note: The filter runs in a subshell, and as such it cannot be
36+
# used to affect the global environment of the main script.
3637
# * `--filter=/<regex>/` -- Matches each argument value against the regex. If
3738
# the regex doesn't match, the argument is rejected.
38-
# * `--enum=<spec>` -- Matches each argument value against a set of valid names.
39-
# `<spec>` must be a space-separated list of names, e.g. `--enum='yes no
40-
# maybe'`.
39+
# * `--enum[]=<spec>` -- Matches each argument value against a set of valid
40+
# names. `<spec>` must be a non-empty list of values, in the usual multi-value
41+
# form accepted by this system, e.g. `--enum[]='yes no "maybe so"'`.
4142
#
4243
# Some argument-definers also accept these options:
4344
# * `--default=<value>` -- Specifies a default value for an argument or option
@@ -151,7 +152,7 @@ function opt-multi {
151152
local optRequired=0
152153
local optVar=''
153154
local args=("$@")
154-
_argproc_janky-args call enum filter required var \
155+
_argproc_janky-args call enum[] filter required var \
155156
|| return 1
156157

157158
local specName=''
@@ -218,7 +219,7 @@ function opt-value {
218219
local optRequired=0
219220
local optVar=''
220221
local args=("$@")
221-
_argproc_janky-args call default enum filter required var \
222+
_argproc_janky-args call default enum[] filter required var \
222223
|| return 1
223224

224225
local specName=''
@@ -252,7 +253,7 @@ function positional-arg {
252253
local optRequired=0
253254
local optVar=''
254255
local args=("$@")
255-
_argproc_janky-args call default enum filter required var \
256+
_argproc_janky-args call default enum[] filter required var \
256257
|| return 1
257258

258259
local specName=''
@@ -361,7 +362,7 @@ function rest-arg {
361362
local optFilter=''
362363
local optVar=''
363364
local args=("$@")
364-
_argproc_janky-args call enum filter var \
365+
_argproc_janky-args call enum[] filter var \
365366
|| return 1
366367

367368
local specName=''
@@ -658,6 +659,31 @@ function _argproc_error-coda {
658659
fi
659660
}
660661

662+
# Helper (called by code produced by `_argproc_handler-body`) which performs
663+
# a filter call. Upon success, prints all the filtered values.
664+
function _argproc_filter-call {
665+
local desc="$1"
666+
local filter="$2"
667+
shift 2
668+
669+
if [[ ${filter} =~ ^\{(.*)\}$ ]]; then
670+
# Kinda gross, but this makes it easy to call the filter code block.
671+
eval "function _argproc_filter-call:inner {
672+
${BASH_REMATCH[1]}
673+
}"
674+
filter='_argproc_filter-call:inner'
675+
fi
676+
677+
local arg result
678+
for arg in "$@"; do
679+
if ! result=("$("${filter}" "${arg}")"); then
680+
error-msg "Invalid value for ${desc}: ${arg}"
681+
return 1
682+
fi
683+
vals -- "${result}"
684+
done
685+
}
686+
661687
# Produces an argument handler body, from the given components.
662688
function _argproc_handler-body {
663689
local specName="$1"
@@ -675,16 +701,20 @@ function _argproc_handler-body {
675701
"${desc}" "${filter}"
676702
)")
677703
elif [[ ${filter} != '' ]]; then
678-
# Add a loop to call the filter function on each argument.
679-
result+=(
680-
"$(printf '
681-
local _argproc_value _argproc_args=()
682-
for _argproc_value in "$@"; do
683-
_argproc_args+=("$(%s "${_argproc_value}")") || return 1
684-
done
685-
set -- "${_argproc_args[@]}"' \
686-
"${filter}")"
687-
)
704+
# Add a call to perform the filtering.
705+
local desc="$(_argproc_arg-description "${specName}")"
706+
result+=("$(printf '
707+
local _argproc_args
708+
_argproc_args="$(_argproc_filter-call %q %q "$@")" \
709+
&& set-array-from-vals _argproc_args "${_argproc_args}" \
710+
|| {
711+
local error="$?"
712+
error-msg --suppress-cmd
713+
return "${error}"
714+
}
715+
set -- "${_argproc_args[@]}"' \
716+
"${desc}" "${filter}"
717+
)")
688718
fi
689719

690720
if [[ ${callFunc} =~ ^\{(.*)\}$ ]]; then
@@ -733,14 +763,19 @@ function _argproc_janky-args {
733763
local gotDefault=0
734764
local a
735765

766+
# TEMP: Remove spec mod once use sites are migrated.
767+
if [[ ${argSpecs} =~ ' enum[] ' ]]; then
768+
argSpecs+='enum '
769+
fi
770+
736771
for a in "${args[@]}"; do
737772
if (( optsDone )); then
738773
args+=("${a}")
739774
continue
740775
fi
741776

742777
if [[ ${a} =~ ^--. ]]; then
743-
if ! [[ ${a} =~ ^--([a-z][-a-z]+)(=.*)?$ ]]; then
778+
if ! [[ ${a} =~ ^--([a-z][-a-z]+\[?\]?)(=.*)?$ ]]; then
744779
error-msg --file-line=2 "Invalid option syntax: ${a}"
745780
_argproc_declarationError=1
746781
return 1
@@ -767,25 +802,14 @@ function _argproc_janky-args {
767802
&& optDefault="${BASH_REMATCH[1]}" \
768803
|| argError=1
769804
;;
770-
enum)
771-
if [[ ${value} =~ ^=(' '*([-_:.a-zA-Z0-9]+' '*)+)$ ]]; then
772-
# Re-form as a filter expression.
773-
value="${BASH_REMATCH[1]}"
774-
optFilter=''
775-
while [[ ${value} =~ ^' '*([^ ]+)' '*(.*)$ ]]; do
776-
optFilter+="|${BASH_REMATCH[1]}"
777-
value="${BASH_REMATCH[2]}"
778-
done
779-
# `:1` to drop the initial `|`.
780-
optFilter="/^(${optFilter:1})\$/"
781-
# "Escape" `.` so it's not treated as regex syntax.
782-
optFilter="${optFilter//./[.]}"
783-
else
805+
# TEMP: Remove plain `enum` once use sites are migrated.
806+
enum|enum[])
807+
if ! _argproc_parse-enum "${value#=}"; then
784808
argError=1
785809
fi
786810
;;
787811
filter)
788-
[[ ${value} =~ ^=(/.*/|[_a-zA-Z][-_:a-zA-Z0-9]*)$ ]] \
812+
[[ ${value} =~ ^=(/.*/|\{.*\}|[_a-zA-Z][-_:a-zA-Z0-9]*)$ ]] \
789813
&& optFilter="${BASH_REMATCH[1]}" \
790814
|| argError=1
791815
;;
@@ -858,6 +882,32 @@ function _argproc_janky-args {
858882
fi
859883
}
860884

885+
# Parses a single enumeration-set value. Upon success sets a `optFilter`
886+
# (presumed local in the calling scope) to "return" a filter expression that
887+
# matches the specified enumeration.
888+
function _argproc_parse-enum {
889+
local value="$1"
890+
local values
891+
892+
set-array-from-vals values "${value}" \
893+
|| return "$?"
894+
895+
if (( ${#values[@]} == 0 )); then
896+
# Error: Must have at least one value.
897+
return 1
898+
fi
899+
900+
optFilter="$(
901+
printf '{ [[ "$1" =~ ^('
902+
local or='' print
903+
for value in "${values[@]}"; do
904+
printf '%s%s' "${or}" "$(vals --dollar -- "${value}")"
905+
or='|'
906+
done
907+
printf $')$ ]] && printf \'%%s\' "$1" }'
908+
)"
909+
}
910+
861911
# Parses a single argument / option spec. `--short` to accept a <short>
862912
# (short-option) character. `--value` to accept a value. `--value-eq` to accept
863913
# a value and leave the `=` in the result (to distinguish unset and
@@ -1022,12 +1072,12 @@ function _argproc_statements-from-args {
10221072
;;
10231073
'[]=')
10241074
# Multi-value option. Parse the value into elements.
1025-
if eval 2>/dev/null "values=(${value})"; then
1075+
if set-array-from-vals values "${value}"; then
10261076
_argproc_statements+=(
10271077
"${handler} $(_argproc_quote "${values[@]}")")
10281078
else
10291079
error-msg "Invalid multi-value syntax for option --${name}:"
1030-
error-msg " ${value}"
1080+
error-msg " $(vals -- "${value}")"
10311081
argError=1
10321082
fi
10331083
;;

0 commit comments

Comments
 (0)