diff --git a/lib/bash/cli/README.md b/lib/bash/cli/README.md index ea3ac32..db63977 100644 --- a/lib/bash/cli/README.md +++ b/lib/bash/cli/README.md @@ -34,7 +34,10 @@ metadata in one source of truth. It is one sourceable file and requires - `base_cli_positional MODEL PATH NAME [required=true|false] [repeatable=true|false] [default=VALUE] [enum=A,B] [validator=FUNCTION] [help=TEXT] [metavar=NAME]` declares a positional argument. A repeatable - positional must be the final positional in its command. + positional must be the final positional in its command. If it has a default, + that value is used and validated when no values are supplied. An explicitly + supplied empty value counts as input and does not select the default; a + default satisfies `required=true` when the caller omits the positional. - `base_cli_help MODEL [PATH]` renders deterministic help to stdout. - `base_cli_parse MODEL -- [ARGV...]` parses and validates an invocation. It returns `0` on success, `2` for usage/validation errors, and publishes the diff --git a/lib/bash/cli/lib_cli.sh b/lib/bash/cli/lib_cli.sh index 40ef9d0..975fecc 100644 --- a/lib/bash/cli/lib_cli.sh +++ b/lib/bash/cli/lib_cli.sh @@ -1278,8 +1278,23 @@ __base_bash_libs_cli_apply_defaults_and_validate__() { return 0 } +__base_bash_libs_cli_apply_positional_default__() { + local model="$1" path="$2" name="$3" default="$4" has_default="$5" required="$6" + + if [[ "$has_default" == 1 ]]; then + __base_bash_libs_cli_validate_value__ "$model" "$path" positional "$name" "$default" || return $? + BASE_BASH_LIBS_CLI_RESULT_POSITIONALS+=("$default") + return 0 + fi + if [[ "$required" =~ ^(1|true|yes)$ ]]; then + __base_bash_libs_cli_error__ "required positional '$name' was not provided." + return $? + fi + return 1 +} + __base_bash_libs_cli_apply_positionals__() { - local model="$1" path="$2" value name index repeatable required default repeat_start + local model="$1" path="$2" value name index repeatable required default default_set repeat_start status local -a __base_bash_libs_cli_positional_names=() __base_bash_libs_cli_collect_positionals__ "$model" "$path" @@ -1295,6 +1310,8 @@ __base_bash_libs_cli_apply_positionals__() { repeatable="$(__base_bash_libs_cli_positional_meta__ "$model" "$path" "$name" repeatable)" required="$(__base_bash_libs_cli_positional_meta__ "$model" "$path" "$name" required)" default="$(__base_bash_libs_cli_positional_meta__ "$model" "$path" "$name" default)" + default_set=0 + [[ -n "${__base_bash_libs_cli_models["$model|positional|$path|meta|$name|default"]+set}" ]] && default_set=1 if [[ "$repeatable" =~ ^(1|true|yes)$ ]]; then repeat_start="$index" while ((index < ${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]})); do @@ -1302,22 +1319,27 @@ __base_bash_libs_cli_apply_positionals__() { __base_bash_libs_cli_validate_value__ "$model" "$path" positional "$name" "$value" || return $? ((index++)) done - if ((index == repeat_start)) && [[ "$required" =~ ^(1|true|yes)$ ]]; then - __base_bash_libs_cli_error__ "required positional '$name' was not provided." - return $? + if ((index == repeat_start)); then + if __base_bash_libs_cli_apply_positional_default__ \ + "$model" "$path" "$name" "$default" "$default_set" "$required"; then + [[ "$default_set" == 1 ]] && index=$((index + 1)) + else + status=$? + ((status == 1)) || return "$status" + fi fi return 0 fi if ((index >= ${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]})); then - if [[ -n "${__base_bash_libs_cli_models["$model|positional|$path|meta|$name|default"]+set}" ]]; then - BASE_BASH_LIBS_CLI_RESULT_POSITIONALS+=("$default") - __base_bash_libs_cli_validate_value__ "$model" "$path" positional "$name" "$default" || return $? - ((index++)) + if __base_bash_libs_cli_apply_positional_default__ \ + "$model" "$path" "$name" "$default" "$default_set" "$required"; then + index=$((index + 1)) continue + else + status=$? + ((status == 1)) && continue + return "$status" fi - [[ "$required" =~ ^(1|true|yes)$ ]] || continue - __base_bash_libs_cli_error__ "required positional '$name' was not provided." - return $? fi value="${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[index]}" __base_bash_libs_cli_validate_value__ "$model" "$path" positional "$name" "$value" || return $? diff --git a/lib/bash/cli/tests/lib_cli.bats b/lib/bash/cli/tests/lib_cli.bats index ed4c939..2465a0d 100644 --- a/lib/bash/cli/tests/lib_cli.bats +++ b/lib/bash/cli/tests/lib_cli.bats @@ -408,6 +408,68 @@ EOF [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[1]}" = working ] } +@test "repeatable positional defaults are applied and validated only when omitted" { + valid_archive() { [[ "$1" == archive ]]; } + + base_cli_model_init direct_repeat_default name=direct-repeat-default + base_cli_command direct_repeat_default run "Run" + base_cli_positional direct_repeat_default run target required=true + base_cli_positional direct_repeat_default run files repeatable=true required=true \ + default=fallback enum=fallback,archive validator=valid_target + + base_cli_parse direct_repeat_default -- run target-value + [ "${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]}" -eq 2 ] + [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[0]}" = target-value ] + [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[1]}" = fallback ] + + base_cli_parse direct_repeat_default -- run target-value archive + [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[1]}" = archive ] + + base_cli_model_init explicit_empty name=explicit-empty + base_cli_command explicit_empty run "Run" + base_cli_positional explicit_empty run values repeatable=true default=fallback + base_cli_parse explicit_empty -- run "" + [ "${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]}" -eq 1 ] + [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[0]}" = "" ] + + base_cli_model_init invalid_enum name=invalid-enum + base_cli_command invalid_enum run "Run" + bats_run base_cli_positional invalid_enum run values repeatable=true default=invalid enum=valid + [ "$status" -eq 2 ] + [[ "$output" == *"default must be one of the declared enum values"* ]] + + base_cli_model_init invalid_validator name=invalid-validator + base_cli_command invalid_validator run "Run" + base_cli_positional invalid_validator run values repeatable=true default=fallback \ + enum=fallback,archive validator=valid_archive + if base_cli_parse invalid_validator -- run >/dev/null 2>&1; then + status=0 + else + status=$? + fi + [ "$status" -eq 2 ] + [ "${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]}" -eq 0 ] + + base_cli_model_init invalid_scalar_default name=invalid-scalar-default + base_cli_command invalid_scalar_default run "Run" + base_cli_positional invalid_scalar_default run value default=fallback validator=valid_archive + if base_cli_parse invalid_scalar_default -- run >/dev/null 2>&1; then + status=0 + else + status=$? + fi + [ "$status" -eq 2 ] + [ "${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]}" -eq 0 ] + + base_cli_declare table_repeat_default \ + 'model|name=table-repeat-default' \ + 'command|path=run|description=Run' \ + 'positional|path=run|name=values|repeatable=true|required=true|default=fallback|enum=fallback,archive' + base_cli_parse table_repeat_default -- run + [ "${#BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[@]}" -eq 1 ] + [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[0]}" = fallback ] +} + @test "quick declarations enforce required repeatable positional tails" { base_cli_declare table_repeat \ 'model|name=table-repeat' \