From 917082ca0aa596f53272681ca26d65e796cbd07f Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:08:40 +0530 Subject: [PATCH] bug: enforce required repeatable positionals (#344) --- CHANGELOG.md | 2 ++ lib/bash/cli/lib_cli.sh | 5 +++-- lib/bash/cli/tests/lib_cli.bats | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c84c7e8..45d39ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and versions are tracked in the repo-root `VERSION` file. ### Fixed +- Enforced required repeatable positional tails based on values consumed by the + repeatable itself, independent of earlier fixed positional arguments. - Made table-driven CLI declaration transactional so any late semantic error restores an existing model exactly or removes a partial new model. - Rejected unreachable CLI command routes and ancestor/child option collisions, diff --git a/lib/bash/cli/lib_cli.sh b/lib/bash/cli/lib_cli.sh index f9f8553..d88c412 100644 --- a/lib/bash/cli/lib_cli.sh +++ b/lib/bash/cli/lib_cli.sh @@ -1175,7 +1175,7 @@ __base_bash_libs_cli_apply_defaults_and_validate__() { } __base_bash_libs_cli_apply_positionals__() { - local model="$1" path="$2" value name index repeatable required default + local model="$1" path="$2" value name index repeatable required default repeat_start __base_bash_libs_cli_collect_positionals__ "$model" "$path" if ((${#__base_bash_libs_cli_positional_names[@]} == 0)); then @@ -1191,12 +1191,13 @@ __base_bash_libs_cli_apply_positionals__() { required="$(__base_bash_libs_cli_positional_meta__ "$model" "$path" "$name" required)" default="$(__base_bash_libs_cli_positional_meta__ "$model" "$path" "$name" default)" 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 == 0)) && [[ "$required" =~ ^(1|true|yes)$ ]]; then + if ((index == repeat_start)) && [[ "$required" =~ ^(1|true|yes)$ ]]; then __base_bash_libs_cli_error__ "required positional '$name' was not provided." return $? fi diff --git a/lib/bash/cli/tests/lib_cli.bats b/lib/bash/cli/tests/lib_cli.bats index c6d3aeb..1d02d04 100644 --- a/lib/bash/cli/tests/lib_cli.bats +++ b/lib/bash/cli/tests/lib_cli.bats @@ -239,6 +239,46 @@ EOF [ "${BASE_BASH_LIBS_CLI_RESULT_POSITIONALS[1]}" = "--looks-like-option" ] } +@test "required repeatable positionals count only their own consumed values" { + base_cli_model_init direct_repeat name=direct-repeat + base_cli_command direct_repeat run "Run" + base_cli_positional direct_repeat run target required=true + base_cli_positional direct_repeat run files required=true repeatable=true + + bats_run base_cli_parse direct_repeat -- run target-only + [ "$status" -eq 2 ] + [[ "$output" == *"required positional 'files' was not provided"* ]] + base_cli_parse direct_repeat -- run target one + base_cli_parse direct_repeat -- run target one two + + base_cli_model_init root_repeat name=root-repeat + base_cli_positional root_repeat '' items required=true repeatable=true + bats_run base_cli_parse root_repeat -- + [ "$status" -eq 2 ] + base_cli_parse root_repeat -- one + base_cli_parse root_repeat -- one two + + base_cli_model_init optional_repeat name=optional-repeat + base_cli_command optional_repeat run "Run" + base_cli_positional optional_repeat run target required=true + base_cli_positional optional_repeat run files repeatable=true + base_cli_parse optional_repeat -- run target-only +} + +@test "quick declarations enforce required repeatable positional tails" { + base_cli_declare table_repeat \ + 'model|name=table-repeat' \ + 'command|path=run|description=Run' \ + 'positional|path=run|name=target|required=true' \ + 'positional|path=run|name=files|required=true|repeatable=true' + + bats_run base_cli_parse table_repeat -- run target-only + [ "$status" -eq 2 ] + [[ "$output" == *"required positional 'files' was not provided"* ]] + base_cli_parse table_repeat -- run target one + base_cli_parse table_repeat -- run target one two +} + @test "run invokes handlers but help and version do not" { local handler_calls=0 demo_handler() {