Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/bash/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 33 additions & 11 deletions lib/bash/cli/lib_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -1295,29 +1310,36 @@ __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
value="${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[index]}"
__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 $?
Expand Down
62 changes: 62 additions & 0 deletions lib/bash/cli/tests/lib_cli.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down
Loading