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
30 changes: 26 additions & 4 deletions lib/bash/cli/lib_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,19 @@ __base_bash_libs_cli_valid_model__() {
}

__base_bash_libs_cli_valid_segment__() {
[[ "${1-}" =~ ^[A-Za-z0-9_-]+$ ]]
[[ "${1-}" == - || "${1-}" =~ ^[A-Za-z0-9_][A-Za-z0-9_-]*$ ]]
}

__base_bash_libs_cli_builtin_option_action__() {
case "${1-}" in
-h | --help) printf 'help' ;;
-V | --version) printf 'version' ;;
*) return 1 ;;
esac
}

__base_bash_libs_cli_is_builtin_option_token__() {
__base_bash_libs_cli_builtin_option_action__ "${1-}" > /dev/null
}

__base_bash_libs_cli_valid_path__() {
Expand Down Expand Up @@ -786,7 +798,8 @@ base_cli_validate_model() {
path="${route%%|token|*}"
token="${route#*|token|}"
name="${__base_bash_libs_cli_models[$key]}"
if ! __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token" found_name found_path found_type ||
if __base_bash_libs_cli_is_builtin_option_token__ "$token" ||
! __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token" found_name found_path found_type ||
[[ "$found_name" != "$name" || "$found_path" != "$path" ]]; then
unreachable_routes+=("option:$path:$token")
fi
Expand Down Expand Up @@ -961,6 +974,10 @@ base_cli_option() {
__base_bash_libs_cli_declaration_usage__ "base_cli_option: invalid option token '$token'."
return 2
fi
if __base_bash_libs_cli_is_builtin_option_token__ "$token"; then
__base_bash_libs_cli_declaration_usage__ "base_cli_option: token '$token' is reserved for built-in CLI behavior."
return 2
fi
if [[ "$token" == *=* ]]; then
__base_bash_libs_cli_declaration_usage__ "base_cli_option: option token '$token' cannot contain '='."
return 2
Expand Down Expand Up @@ -1334,6 +1351,7 @@ __base_bash_libs_cli_apply_positionals__() {
# Usage: base_cli_parse model -- [argv...]
base_cli_parse() {
local model="${1-}" current path="" token option_value name type child_path option_path
local builtin_action
# shellcheck disable=SC2034 # Pass-by-name outputs used only to probe whether an option token is registered.
local probe_name probe_path probe_type
local parse_options=1 parse_commands=1
Expand All @@ -1357,13 +1375,17 @@ base_cli_parse() {
parse_commands=0
continue
fi
if ((parse_options)) && [[ "$current" == -h || "$current" == --help ]]; then
builtin_action=""
if ((parse_options)); then
builtin_action="$(__base_bash_libs_cli_builtin_option_action__ "$current" || true)"
fi
if [[ "$builtin_action" == help ]]; then
BASE_BASH_LIBS_CLI_RESULT_COMMAND="$path"
BASE_BASH_LIBS_CLI_RESULT_ACTION="help"
base_cli_help "$model" "$path"
return $?
fi
if ((parse_options)) && [[ "$current" == -V || "$current" == --version ]]; then
if [[ "$builtin_action" == version ]]; then
if [[ -n "$path" || -z "${__base_bash_libs_cli_models["$model|meta|version"]-}" ]]; then
__base_bash_libs_cli_usage_error__ "$model" "$path" "version is not available for this command."
return 2
Expand Down
52 changes: 52 additions & 0 deletions lib/bash/cli/tests/lib_cli.bats
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,21 @@ EOF
[[ "$output" == *"route 'u' was provided more than once"* ]]
}

@test "a lone dash is accepted as a model name command name and alias" {
base_cli_model_init dash_model name=-
base_cli_validate_model dash_model

base_cli_model_init dash_command name=dash-command
base_cli_command dash_command - "Read standard input"
base_cli_parse dash_command -- -
[ "$BASE_BASH_LIBS_CLI_RESULT_COMMAND" = - ]

base_cli_model_init dash_alias name=dash-alias
base_cli_command dash_alias stdin "Read standard input" aliases=-
base_cli_parse dash_alias -- -
[ "$BASE_BASH_LIBS_CLI_RESULT_COMMAND" = stdin ]
}

@test "ancestor and child option names and tokens cannot shadow each other" {
base_cli_model_init ancestor_first name=ancestor-first
base_cli_command ancestor_first child "Child"
Expand Down Expand Up @@ -530,6 +545,36 @@ EOF
[ "$status" -eq 2 ]
}

@test "built-in option tokens and option-shaped command routes are unreachable" {
local before token path

base_cli_model_init reserved name=reserved version=2.0.0
base_cli_command reserved run "Run"
before="$(model_registry_dump reserved)"
for path in '' run; do
for token in -h --help -V --version; do
bats_run base_cli_option reserved "$path" custom value "$token"
[ "$status" -eq 2 ]
[[ "$output" == *"reserved for built-in CLI behavior"* ]]
[ "$(model_registry_dump reserved)" = "$before" ]
done
done

bats_run base_cli_command reserved --diagnose "Unreachable route"
[ "$status" -eq 2 ]
bats_run base_cli_command reserved run/-diagnose "Unreachable nested route"
[ "$status" -eq 2 ]
bats_run base_cli_command reserved manage "Manage" aliases=-m
[ "$status" -eq 2 ]

bats_run base_cli_declare reserved_table \
'model|name=reserved-table|version=2.0.0' \
'command|path=run|description=Run' \
'option|path=run|name=help|type=flag|tokens=--help'
[ "$status" -eq 2 ]
[ "$(model_registry_dump reserved_table)" = "" ]
}

@test "model validation detects unreachable command and option routes" {
base_cli_model_init routes name=routes
base_cli_command routes user "User" aliases=u
Expand All @@ -541,11 +586,18 @@ EOF
__base_bash_libs_cli_models['routes|option|child|meta|child_mode|tokens']='--mode'
__base_bash_libs_cli_models['routes|option|child|token|--mode']=child_mode

__base_bash_libs_cli_models['routes|option|child|meta|built_in|type']=flag
__base_bash_libs_cli_models['routes|option|child|meta|built_in|tokens']='--help'
__base_bash_libs_cli_models['routes|option|child|token|--help']=built_in
__base_bash_libs_cli_option_name_index['routes|built_in']=child
__base_bash_libs_cli_option_token_index['routes|--help']=child

bats_run base_cli_validate_model routes

[ "$status" -eq 2 ]
[[ "$output" == *"alias:user:u"* ]]
[[ "$output" == *"option:child:--mode"* ]]
[[ "$output" == *"option:child:--help"* ]]
}

@test "option validators may render help without corrupting required-option traversal" {
Expand Down
Loading