From 5c51bccd9508ee3c5d417436c10d6f8b0e1da9f2 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Mon, 23 Oct 2023 15:49:43 -0700 Subject: [PATCH] Updates from upstream. --- scripts/lib/bashy-core/arg-processor.sh | 166 ++++++++++++++---- .../lib/bashy-core/helpy/print-all-commands | 2 +- scripts/lib/bashy-core/meta.sh | 9 +- scripts/lib/bashy-core/misc.sh | 33 +++- 4 files changed, 164 insertions(+), 46 deletions(-) diff --git a/scripts/lib/bashy-core/arg-processor.sh b/scripts/lib/bashy-core/arg-processor.sh index bf96a06a8..816847e8c 100644 --- a/scripts/lib/bashy-core/arg-processor.sh +++ b/scripts/lib/bashy-core/arg-processor.sh @@ -17,8 +17,9 @@ # `` are always optional and (independently) sometimes prohibited, # depending on the definition function. # -# The public argument-defining functions also allow these options, of which -# at least one of `--call` or `--var` must be used. When multiple of these are +# Most public argument-defining functions also all allow these options, of which +# at least one must be used. When both are used, the `--call` is performed +# first, and then the `--var` setting. # present, evaluation order is filter then call then variable setting. # * `--call=` or `--call={}` -- Calls the named function passing it # the argument value(s), or runs the indicated code snippet. If the call @@ -26,7 +27,8 @@ # parameter references (`$1` `$@` `set ` etc.) are available. # * `--var=` -- Sets the named variable to the argument value(s). # -# Value-accepting argument definers allow these additional options: +# Value-accepting argument definers allow these additional options to pre-filter +# a value, before it gets set or passed to a `--call`: # * `--filter=` -- Calls the named function passing it a single argument # value; the function must output a replacement value. If the call fails, the # argument is rejected. Note: The filter function runs in a subshell, and as @@ -106,6 +108,36 @@ function opt-action { fi } +# Declares an "alias" option, which expands into an arbitrary set of other +# options. On a commandline, alias options do not accept values (because values, +# if any, are provided in the expansion). Similarly, and unlike most of the +# argument-definining functions, alias options cannot be specified with either +# `--call` or `--var` (because any calls or variable setting is done with the +# expanded options). In fact, _no_ options are available when defining an alias. +function opt-alias { + local args=("$@") + _argproc_janky-args --multi-arg \ + || return 1 + + local specName='' + local specAbbrev='' + _argproc_parse-spec --abbrev "${args[0]}" \ + || return 1 + + args=("${args[@]:1}") + + local arg + for arg in "${args[@]}"; do + if [[ ! (${arg} =~ ^-) ]]; then + error-msg --file-line=1 "Invalid alias expansion: ${arg}" + return 1 + fi + done + + _argproc_define-alias-arg --option "${specName}" "${specAbbrev}" \ + "${args[@]}" +} + # Declares a "choice" option set, consisting of one or more options. On a # commandline, no choice option accepts a value (because the option name itself # implies the value). If left unspecified, the initial variable value for a @@ -483,11 +515,45 @@ function _argproc_define-abbrev { local abbrevChar="$1" local specName="$2" - eval 'function _argproc:abbrev-'"${abbrevChar}"' { - _argproc:long-'"${specName}"' "$@" + eval 'function _argproc:short-alias-'"${abbrevChar}"' { + echo --'"${specName}"' }' } +# Defines an activation function for an alias. +function _argproc_define-alias-arg { + if [[ $1 == '--option' ]]; then + shift + else + # `--option` is really defined here for parallel structure, not utility. + error-msg --file-line=1 'Not supported.' + return 1 + fi + + local specName="$1" + local abbrevChar="$2" + shift 2 + local args=("$@") + + _argproc_set-arg-description "${specName}" option || return 1 + + local desc="$(_argproc_arg-description "${specName}")" + local handlerName="_argproc:alias-${specName}" + eval 'function '"${handlerName}"' { + if (( $# > 0 )); then + error-msg "Value not allowed for '"${desc}"'." + return 1 + fi + printf "%q\\n" '"$(_argproc_quote "${args[@]}")"' + }' + + if [[ ${abbrevChar} != '' ]]; then + eval 'function _argproc:short-alias-'"${abbrevChar}"' { + '"${handlerName}"' "$@" + }' + fi +} + # Defines an activation function for a multi-value argument. function _argproc_define-multi-value-arg { local isOption=0 isRest=0 @@ -960,7 +1026,7 @@ function _argproc_set-arg-description { # Builds up a list of statements to evaluate, based on the given arguments. It # is stored in the variable `_argproc_statements`, which is assumed to be -# declared by its caller. +# declared `local` by its caller. # # Note: This arrangement, where argument parsing is done in a separate # function and as a separate pass from evaluation, makes it possible to use @@ -968,7 +1034,7 @@ function _argproc_set-arg-description { # read. function _argproc_statements-from-args { local argError=0 - local arg handler name value values + local arg handler name assign value values # This is used for required-argument checking. _argproc_statements+=($'local _argproc_receivedArgNames=\'\'') @@ -983,35 +1049,55 @@ function _argproc_statements-from-args { elif [[ ${arg} == '' || ${arg} =~ ^-[0-9]*$ || ${arg} =~ ^[^-] ]]; then # Non-option argument. break - elif [[ ${arg} =~ ^--([-a-zA-Z0-9]+)(=.*)?$ ]]; then - # Long-form no- or single-value option. + elif [[ ${arg} =~ ^--([-a-zA-Z0-9]+)(('[]='|=)(.*))?$ ]]; then + # Long-form option. name="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - handler="_argproc:long-${name}" - if ! declare -F "${handler}" >/dev/null; then - error-msg "Unknown option: --${name}" - argError=1 - elif [[ ${value} == '' ]]; then - _argproc_statements+=("${handler}") + assign="${BASH_REMATCH[3]}" + value="${BASH_REMATCH[4]}" + if handler="_argproc:long-${name}" \ + && declare -F "${handler}" >/dev/null; then + case "${assign}" in + '') + # No-value option. + _argproc_statements+=("${handler}") + ;; + '=') + # Single-value option. + _argproc_statements+=( + "${handler} $(_argproc_quote "${value}")") + ;; + '[]=') + # Multi-value option. Parse the value into elements. + if eval 2>/dev/null "values=(${value})"; then + _argproc_statements+=( + "${handler} $(_argproc_quote "${values[@]}")") + else + error-msg "Invalid multi-value syntax for option --${name}:" + error-msg " ${value}" + argError=1 + fi + ;; + esac + elif handler="_argproc:alias-${name}" \ + && declare -F "${handler}" >/dev/null; then + # Alias option; must not be passed any values. + if [[ ${assign} == '' ]]; then + # Parse the output of `handler` into new options, and + # "unshift" them onto `$@`. + if eval 2>/dev/null "values=($("${handler}"))"; then + shift # Shift the alias option away. + set -- shifted-away-below "${values[@]}" "$@" + else + error-msg "Could not expand alias option: --${name}" + argError=1 + fi + else + error-msg "Cannot pass values to alias option: --${name}" + argError=1 + fi else - # `:1` to drop the `=` from the start of `value`. - _argproc_statements+=("${handler} $(_argproc_quote "${value:1}")") - fi - elif [[ ${arg} =~ ^--([-a-zA-Z0-9]+)'[]='(.*)$ ]]; then - # Long-form multi-value option. - name="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - handler="_argproc:long-${name}" - if ! declare -F "${handler}" >/dev/null; then error-msg "Unknown option: --${name}" argError=1 - else - # Parse the value into elements. - eval 2>/dev/null "values=(${value})" || { - error-msg "Invalid multi-value syntax for option --${name}:" - error-msg " ${value}" - } - _argproc_statements+=("${handler} $(_argproc_quote "${values[@]}")") fi elif [[ $arg =~ ^-([a-zA-Z0-9]+)$ ]]; then # Short-form option. @@ -1019,15 +1105,23 @@ function _argproc_statements-from-args { while [[ ${arg} =~ ^(.)(.*)$ ]]; do name="${BASH_REMATCH[1]}" arg="${BASH_REMATCH[2]}" - handler="_argproc:abbrev-${name}" - if ! declare -F "${handler}" >/dev/null; then + if handler="_argproc:short-alias-${name}" \ + && declare -F "${handler}" >/dev/null; then + # Parse the output of `handler` into new options, and + # "unshift" them onto `$@`. + if eval 2>/dev/null "values=($("${handler}"))"; then + shift # Shift the alias option away. + set -- shifted-away-below "${values[@]}" "$@" + else + error-msg "Could not expand alias option: --${name}" + argError=1 + fi + else error-msg "Unknown option: -${name}" argError=1 # Break, to avoid spewing a ton of errors in case of a pilot # error along the lines of `-longOptionName`. break - else - _argproc_statements+=("${handler}") fi done else diff --git a/scripts/lib/bashy-core/helpy/print-all-commands b/scripts/lib/bashy-core/helpy/print-all-commands index ed7fe2be0..e8fa6b2bd 100755 --- a/scripts/lib/bashy-core/helpy/print-all-commands +++ b/scripts/lib/bashy-core/helpy/print-all-commands @@ -41,7 +41,7 @@ unitNames=(${unitNames}) if (( ${#unitNames[@]} == 0 )); then lsArgs=(--depth=2) else - lsArgs=(--depth=1 "${unitNames[@]}") + lsArgs=(--depth=1 -- "${unitNames[@]}") fi baseDir="$(this-base-library-dir)" diff --git a/scripts/lib/bashy-core/meta.sh b/scripts/lib/bashy-core/meta.sh index 74a313ba5..1d9fbb56b 100644 --- a/scripts/lib/bashy-core/meta.sh +++ b/scripts/lib/bashy-core/meta.sh @@ -54,9 +54,9 @@ function base-dir { # calling script. It is an error to try to get the directory before having set # it. function subproject-dir { - if [[ $1 =~ --set=(.*)$ ]]; then + if [[ $1 =~ ^--set=(.*)$ ]]; then local setDir="${BASH_REMATCH[1]}" - if [[ ${setDir} =~ [^/] ]]; then + if [[ ${setDir} =~ ^[^/] ]]; then local callerDir="$(dirname "$(readlink -f -- "${BASH_SOURCE[1]}")")" setDir="${callerDir}/${setDir}" fi @@ -137,8 +137,9 @@ function this-cmd-path { echo "${_bashy_cmdPath}" } -# Gets the full absolute path to this command's base library directory. This is the `lib` -# directory containing the unit of this command. +# Gets the full absolute path to this command's base library directory. This is +# the `lib` directory containing the core `_init` which set the current shell +# environment up. function this-base-library-dir { echo "${_bashy_libDir}" } diff --git a/scripts/lib/bashy-core/misc.sh b/scripts/lib/bashy-core/misc.sh index e2194108d..dbedd8dad 100644 --- a/scripts/lib/bashy-core/misc.sh +++ b/scripts/lib/bashy-core/misc.sh @@ -52,18 +52,41 @@ function define-usage { } # Splits a multi-line string into an array. Assigns the indicated variable. -# Note: This ignores blank lines. +# This ignores blank lines by default. With the option `--nl-terminated`, this +# instead expects every line to be newline-terminated and will report an error +# if the last character in the given string is _not_ a newline. function set-array-from-lines { # Note: Because we use `eval`, local variables are given name prefixes to # avoid conflicts with the caller. + local _bashy_nlTerm=0 + if [[ $1 == --nl-terminated ]]; then + _bashy_nlTerm=1 + shift + fi + local _bashy_name="$1" local _bashy_value="$2" + local _bashy_parsed=() + if (( _bashy_nlTerm )); then + while [[ ${_bashy_value} =~ ^([^$'\n']*)$'\n'(.*)$ ]]; do + _bashy_parsed+=("${BASH_REMATCH[1]}") + _bashy_value="${BASH_REMATCH[2]}" + done + if [[ ${_bashy_value} != '' ]]; then + error-msg --file-line=1 'Last line unterminated.' + return 1 + fi + else + while [[ ${_bashy_value} =~ ^$'\n'*([^$'\n']+)(.*)$ ]]; do + _bashy_parsed+=("${BASH_REMATCH[1]}") + _bashy_value="${BASH_REMATCH[2]}" + done + fi + # No choice but `eval` for Bash-3.2 compatibility. - local _bashy_oldIfs="${IFS}" - IFS=$'\n' - eval "${_bashy_name}=(\${_bashy_value})" - IFS="${_bashy_oldIfs}" + eval "${_bashy_name}=(\"\${_bashy_parsed[@]}\")" + return "$?" } # Sorts an array in-place.