Skip to content

Commit

Permalink
--enum cleanup (#29)
Browse files Browse the repository at this point in the history
This PR is the follow-on from the previous one. It removes the non-`[]`
`--enum` option, and also makes the regex-style filter code get
evaluated using the same helper as other filters.
  • Loading branch information
danfuzz authored Oct 26, 2023
2 parents f039c11 + 06a931e commit 1945f45
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 51 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Breaking changes:
* `bashy-core`:
* `arg-processor`:
* Tightened up syntax for passing multi-value arguments.
* Renamed `--eval=` to `--eval[]=`, to be the same as how options defined by
the system work.
* Reworked `--eval` to be a multi-value option in the same way that the
system lets clients define them. That is, it's now `--enum[]=` instead
of `--enum=`.

Other notable changes:
* `bashy-core`:
Expand Down
77 changes: 33 additions & 44 deletions scripts/lib/bashy-core/arg-processor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
# is rejected. Note: The filter runs in a subshell, and as such it cannot be
# used to affect the global environment of the main script.
# * `--filter=/<regex>/` -- Matches each argument value against the regex. If
# the regex doesn't match, the argument is rejected.
# the regex doesn't match, the argument is rejected. The regex must be
# non-empty.
# * `--enum[]=<spec>` -- Matches each argument value against a set of valid
# names. `<spec>` must be a non-empty list of values, in the usual multi-value
# form accepted by this system, e.g. `--enum[]='yes no "maybe so"'`.
Expand Down Expand Up @@ -666,22 +667,40 @@ function _argproc_filter-call {
local filter="$2"
shift 2

if [[ ${filter} =~ ^\{(.*)\}$ ]]; then
# Kinda gross, but this makes it easy to call the filter code block.
eval "function _argproc_filter-call:inner {
${BASH_REMATCH[1]}
# Kinda gross, but converting a non-call form to a function makes the
# evaluation much more straightforward.
local definedFunc=1 filterCall='_argproc_filter-call:inner'
if [[ ${filter} =~ ^/(.*)/$ ]]; then
filter="$(vals -- "${BASH_REMATCH[1]}")"
eval "function ${filterCall} {
local _argproc_regex=${filter}
[[ \$1 =~ \${_argproc_regex} ]] && printf '%s\\n' \"\$1\"
}"
elif [[ ${filter} =~ ^\{(.*)\}$ ]]; then
filter="${BASH_REMATCH[1]}"
eval "function ${filterCall} {
${filter}
}"
filter='_argproc_filter-call:inner'
else
definedFunc=0
filterCall="${filter}"
fi

local arg result
local arg result error=0
for arg in "$@"; do
if ! result=("$("${filter}" "${arg}")"); then
if ! result=("$("${filterCall}" "${arg}")"); then
error-msg "Invalid value for ${desc}: ${arg}"
return 1
error=1
break
fi
vals -- "${result}"
done

if (( definedFunc )); then
unset -f "${filterCall}"
fi

return "${error}"
}

# Produces an argument handler body, from the given components.
Expand All @@ -692,16 +711,8 @@ function _argproc_handler-body {
local varName="$4"
local result=()

if [[ ${filter} =~ ^/(.*)/$ ]]; then
# Add a call to perform the regex check on each argument.
filter="${BASH_REMATCH[1]}"
local desc="$(_argproc_arg-description "${specName}")"
result+=("$(printf \
'_argproc_regex-filter-check %q %q "$@" || return "$?"\n' \
"${desc}" "${filter}"
)")
elif [[ ${filter} != '' ]]; then
# Add a call to perform the filtering.
if [[ ${filter} != '' ]]; then
# Add a call to perform the filtering on all the arguments.
local desc="$(_argproc_arg-description "${specName}")"
result+=("$(printf '
local _argproc_args
Expand All @@ -720,7 +731,7 @@ function _argproc_handler-body {
if [[ ${callFunc} =~ ^\{(.*)\}$ ]]; then
# Add a compound statement for the code block.
result+=(
"$(printf '{\n%s\n} || return "$?"\n' "${BASH_REMATCH[1]}")"
"$(printf '{\n%s\n} || return "$?"' "${BASH_REMATCH[1]}")"
)
elif [[ ${callFunc} != '' ]]; then
result+=(
Expand Down Expand Up @@ -763,11 +774,6 @@ function _argproc_janky-args {
local gotDefault=0
local a

# TEMP: Remove spec mod once use sites are migrated.
if [[ ${argSpecs} =~ ' enum[] ' ]]; then
argSpecs+='enum '
fi

for a in "${args[@]}"; do
if (( optsDone )); then
args+=("${a}")
Expand Down Expand Up @@ -802,14 +808,13 @@ function _argproc_janky-args {
&& optDefault="${BASH_REMATCH[1]}" \
|| argError=1
;;
# TEMP: Remove plain `enum` once use sites are migrated.
enum|enum[])
enum[])
if ! _argproc_parse-enum "${value#=}"; then
argError=1
fi
;;
filter)
[[ ${value} =~ ^=(/.*/|\{.*\}|[_a-zA-Z][-_:a-zA-Z0-9]*)$ ]] \
[[ ${value} =~ ^=(/.+/|\{.*\}|[_a-zA-Z][-_:a-zA-Z0-9]*)$ ]] \
&& optFilter="${BASH_REMATCH[1]}" \
|| argError=1
;;
Expand Down Expand Up @@ -969,22 +974,6 @@ function _argproc_parse-spec {
fi
}

# Helper (called by code produced by `_argproc_handler-body`) which performs
# a regex filter check.
function _argproc_regex-filter-check {
local desc="$1"
local regex="$2"
shift 2

local arg
for arg in "$@"; do
if [[ ! (${arg} =~ ${regex}) ]]; then
error-msg "Invalid value for ${desc}: ${arg}"
return 1
fi
done
}

# Sets the description of the named argument based on its type. This function
# will fail if an argument with the given name was already defined.
function _argproc_set-arg-description {
Expand Down
14 changes: 9 additions & 5 deletions scripts/lib/bashy-core/misc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function set-array-from-lines {

# Reverse of `vals`: Assigns parsed elements of the given multi-value string
# (as produced by `vals` or similar) into the indicated variable, as an array.
# Values must be separated by at least one whitespace character.
function set-array-from-vals {
if (( $# != 2 )); then
error-msg --file-line=1 'Missing argument(s) to `set-array-from-vals`.'
Expand All @@ -102,16 +103,19 @@ function set-array-from-vals {
local _bashy_name="$1"
local _bashy_value="$2"

local _bashy_notsp=$'[^ \n\r\t]'
local _bashy_space=$'[ \n\r\t]'

# Trim _ending_ whitespace, and prefix `value` with a space, the latter to
# maintain the constraint that values are space-separated.
if [[ ${_bashy_value} =~ ^(.*[^ ])' '+$ ]]; then
# maintain the constraint that values are whitespace-separated.
if [[ ${_bashy_value} =~ ^(.*${bashy_notsp})${_bashy_space}+$ ]]; then
_bashy_value=" ${BASH_REMATCH[1]}"
else
_bashy_value=" ${_bashy_value}"
fi

local _bashy_values=() _bashy_print
while [[ ${_bashy_value} =~ ^' '+([^ ].*)$ ]]; do
while [[ ${_bashy_value} =~ ^${_bashy_space}+(${bashy_notsp}.*)$ ]]; do
_bashy_value="${BASH_REMATCH[1]}"
if [[ ${_bashy_value} =~ ^([-+=_:./%@a-zA-Z0-9]+)(.*)$ ]]; then
_bashy_values+=("${BASH_REMATCH[1]}")
Expand All @@ -123,13 +127,13 @@ function set-array-from-vals {
printf -v _bashy_print '%q' "${BASH_REMATCH[1]}"
_bashy_values+=("${_bashy_print}")
_bashy_value="${BASH_REMATCH[2]}"
elif [[ ${_bashy_value} =~ ^(\$\'([^\']|\\\')*\')(.*)$ ]]; then
elif [[ ${_bashy_value} =~ ^(\$\'([^\'\\]|\\.)*\')(.*)$ ]]; then
_bashy_values+=("${BASH_REMATCH[1]}")
_bashy_value="${BASH_REMATCH[3]}"
fi
done

if ! [[ ${_bashy_value} =~ ^' '*$ ]]; then
if ! [[ ${_bashy_value} =~ ^${_bashy_space}*$ ]]; then
return 1
fi

Expand Down
115 changes: 115 additions & 0 deletions tests/02-core/02-arg-processor/19-filter-regex/expect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
## nop filter, empty value

### stdout
```
value: ''
```

### exit: 0

- - - - - - - - - -

## nop filter, non-empty value

### stdout
```
value: beep
```

### exit: 0

- - - - - - - - - -

## "non-empty" filter, empty value

### stderr
```
the-cmd: Invalid value for option --non-empty:
the-cmd -- test command
```

### exit: 1

- - - - - - - - - -

## "non-empty" filter, non-empty value

### stdout
```
value: beep
```

### exit: 0

- - - - - - - - - -

## single-quotes in filter, matching value

### stdout
```
value: $'beep \'x\' boop'
```

### exit: 0

- - - - - - - - - -

## single-quotes in filter, non-matching value

### stderr
```
the-cmd: Invalid value for option --looks-like-sq-string: beep x boop
the-cmd -- test command
```

### exit: 1

- - - - - - - - - -

## double-quotes in filter, matching value

### stdout
```
value: 'beep "x" boop'
```

### exit: 0

- - - - - - - - - -

## double-quotes in filter, non-matching value

### stderr
```
the-cmd: Invalid value for option --looks-like-dq-string: beep x boop
the-cmd -- test command
```

### exit: 1

- - - - - - - - - -

## dollar-substitution-looking thing in filter, matching value

### stdout
```
value: 'yes abc'
```

### exit: 0

- - - - - - - - - -

## dollar-substitution-looking thing in filter, non-matching value

### stderr
```
the-cmd: Invalid value for option --looks-like-dollar: no abczz
the-cmd -- test command
```

### exit: 1
1 change: 1 addition & 0 deletions tests/02-core/02-arg-processor/19-filter-regex/info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests of regex-style `--filter`.
34 changes: 34 additions & 0 deletions tests/02-core/02-arg-processor/19-filter-regex/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
# Copyright 2022-2023 the Bashy-lib Authors (Dan Bornstein et alia).
# SPDX-License-Identifier: Apache-2.0

[[ "$(readlink -f "$0")" =~ ^(.*/tests/) ]] && . "${BASH_REMATCH[1]}_test-init.sh" || exit 1

cmd="$(this-cmd-dir)/the-cmd"

call-and-log-as-test 'nop filter, empty value' \
"${cmd}" --nop=''

call-and-log-as-test 'nop filter, non-empty value' \
"${cmd}" --nop=beep

call-and-log-as-test '"non-empty" filter, empty value' \
"${cmd}" --non-empty=''

call-and-log-as-test '"non-empty" filter, non-empty value' \
"${cmd}" --non-empty=beep

# These test that the regex syntax accepted isn't treated like Bash source, in
# that quote marks are treated as literals, and `$` always means end-of-input.
call-and-log-as-test 'single-quotes in filter, matching value' \
"${cmd}" --looks-like-sq-string=$'beep \'x\' boop'
call-and-log-as-test 'single-quotes in filter, non-matching value' \
"${cmd}" --looks-like-sq-string=$'beep x boop'
call-and-log-as-test 'double-quotes in filter, matching value' \
"${cmd}" --looks-like-dq-string=$'beep \"x\" boop'
call-and-log-as-test 'double-quotes in filter, non-matching value' \
"${cmd}" --looks-like-dq-string=$'beep x boop'
call-and-log-as-test 'dollar-substitution-looking thing in filter, matching value' \
"${cmd}" --looks-like-dollar=$'yes abc'
call-and-log-as-test 'dollar-substitution-looking thing in filter, non-matching value' \
"${cmd}" --looks-like-dollar=$'no abczz'
28 changes: 28 additions & 0 deletions tests/02-core/02-arg-processor/19-filter-regex/the-cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Copyright 2022-2023 the Bashy-lib Authors (Dan Bornstein et alia).
# SPDX-License-Identifier: Apache-2.0

[[ "$(readlink -f "$0")" =~ ^(.*/tests/) ]] && . "${BASH_REMATCH[1]}_init.sh" || exit 1


#
# Argument parsing
#

define-usage $'
${name} -- test command
This is a test command.
'

opt-value --var=value --filter='/^/' nop
opt-value --var=value --filter='/./' non-empty
opt-value --var=value --filter=$'/\'x\'/' looks-like-sq-string
opt-value --var=value --filter='/"x"/' looks-like-dq-string

Z='zzz'
opt-value --var=value --filter=$'/abc$Z?/' looks-like-dollar

process-args "$@" || exit "$?"

echo "value: $(vals "${value}")"
Loading

0 comments on commit 1945f45

Please sign in to comment.