diff --git a/lib/bash/cli/lib_cli.sh b/lib/bash/cli/lib_cli.sh index 40ef9d0..00e4f36 100644 --- a/lib/bash/cli/lib_cli.sh +++ b/lib/bash/cli/lib_cli.sh @@ -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__() { @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/bash/cli/tests/lib_cli.bats b/lib/bash/cli/tests/lib_cli.bats index ed4c939..3dcf580 100644 --- a/lib/bash/cli/tests/lib_cli.bats +++ b/lib/bash/cli/tests/lib_cli.bats @@ -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" @@ -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 @@ -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" {