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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and versions are tracked in the repo-root `VERSION` file.

- Enforced required repeatable positional tails based on values consumed by the
repeatable itself, independent of earlier fixed positional arguments.
- Made CLI completion consume option values and stop command and option
interpretation after `--`, matching the runtime parser's path semantics.
- 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,
Expand Down
40 changes: 36 additions & 4 deletions lib/bash/cli/lib_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,10 @@ __base_bash_libs_cli_completion_add__() {
# base_cli_complete - Prints completion candidates, one per line.
# Usage: base_cli_complete model -- [complete argv including the current prefix]
base_cli_complete() {
local model="${1-}" current prefix path="" word token option_path name index
local model="${1-}" current prefix path="" word token option_path name type child_path index
# shellcheck disable=SC2034 # Option lookup path is intentionally unused while resolving completion state.
local found_name found_path found_type
local parse_options=1 pending_value=0 inline_value=0
local -a words=() completed=() children=() __base_bash_libs_cli_option_tokens=()

if (($# < 2)) || [[ "$2" != -- ]]; then
Expand All @@ -1379,10 +1382,39 @@ base_cli_complete() {
if ((${#words[@]} == 0)); then prefix=""; else prefix="${words[${#words[@]} - 1]}"; fi
if ((${#words[@]} > 1)); then completed=("${words[@]:0:${#words[@]}-1}"); fi
for word in "${completed[@]+${completed[@]}}"; do
[[ "$word" == -* ]] && continue
token="$(__base_bash_libs_cli_command_child__ "$model" "$path" "$word")"
[[ -n "$token" ]] && path="$token"
if ((pending_value)); then
pending_value=0
continue
fi
if ((parse_options)) && [[ "$word" == -- ]]; then
parse_options=0
continue
fi
((parse_options)) || continue
if [[ "$word" == -* && "$word" != - ]]; then
token="$word"
inline_value=0
if [[ "$word" == --*=* ]]; then
token="${word%%=*}"
inline_value=1
fi
if __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token" \
found_name found_path found_type; then
[[ "$found_type" == flag || "$inline_value" -eq 1 ]] || pending_value=1
fi
continue
fi
child_path="$(__base_bash_libs_cli_command_child__ "$model" "$path" "$word")"
[[ -n "$child_path" ]] && path="$child_path"
done
((parse_options && !pending_value)) || return 0
if [[ "$prefix" == --*=* ]]; then
token="${prefix%%=*}"
if __base_bash_libs_cli_option_lookup__ "$model" "$path" "$token" \
found_name found_path found_type && [[ "$found_type" != flag ]]; then
return 0
fi
fi
__base_bash_libs_cli_completion_candidates=()
if [[ "$prefix" == -* ]]; then
__base_bash_libs_cli_collect_options__ "$model" "$path"
Expand Down
60 changes: 60 additions & 0 deletions lib/bash/cli/tests/lib_cli.bats
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,66 @@ EOF
[[ "$output" == *"complete -F _demo_complete demo"* ]]
}

@test "completion consumes option values and honors the double-dash boundary" {
base_cli_model_init complete name=complete
base_cli_command complete admin "Administration" aliases=a
base_cli_command complete admin/user "User" aliases=u
base_cli_option complete '' verbose flag --verbose -v
base_cli_option complete '' config value --config
base_cli_option complete '' tag repeatable --tag

bats_run base_cli_complete complete -- --config admin
[ "$status" -eq 0 ]
[ -z "$output" ]
bats_run base_cli_complete complete -- --config admin ""
[ "$status" -eq 0 ]
[[ "$output" == *"admin"* ]]
[[ "$output" != *"user"* ]]

bats_run base_cli_complete complete -- --tag admin
[ "$status" -eq 0 ]
[ -z "$output" ]
bats_run base_cli_complete complete -- --config=admin ""
[ "$status" -eq 0 ]
[[ "$output" == *"admin"* ]]
bats_run base_cli_complete complete -- --config -not-an-option
[ "$status" -eq 0 ]
[ -z "$output" ]
bats_run base_cli_complete complete -- --config "" ""
[ "$status" -eq 0 ]
[[ "$output" == *"admin"* ]]

bats_run base_cli_complete complete -- --verbose ""
[ "$status" -eq 0 ]
[[ "$output" == *"admin"* ]]
bats_run base_cli_complete complete -- a u
[ "$status" -eq 0 ]
[[ "$output" == *"user"* ]]
[[ "$output" == *"u"* ]]

bats_run base_cli_complete complete -- -- admin ""
[ "$status" -eq 0 ]
[ -z "$output" ]
bats_run base_cli_complete complete -- -- ""
[ "$status" -eq 0 ]
[ -z "$output" ]
}

@test "completion keeps partial candidates unique and one per line" {
base_cli_model_init unique_complete name=unique-complete
base_cli_command unique_complete admin "Administration" aliases=adm
base_cli_option unique_complete '' verbose flag --verbose -v

bats_run base_cli_complete unique_complete -- ad
[ "$status" -eq 0 ]
[ "$output" = $'admin\nadm' ]
[ "$(printf '%s\n' "$output" | LC_ALL=C sort | uniq -d)" = "" ]

bats_run base_cli_complete unique_complete -- --v
[ "$status" -eq 0 ]
[ "$output" = --verbose ]
}

@test "public CLI paths preserve caller variables that match internal scratch names" {
local __base_bash_libs_cli_option_name=caller-name
local __base_bash_libs_cli_option_path=caller-path
Expand Down
Loading