Skip to content

Commit 9b5435f

Browse files
zsh: prevent duplicate default REMAINDER spec in generated completions
When completing commands with a subparser that uses nargs=REMAINDER, zsh could emit: _arguments:comparguments:327: doubled rest argument definition: *::: :->repro Root cause: - The zsh template appended the default positional/rest specs (`': :{prefix}_commands'` and `'*::: :->{name}'`) on every function invocation. It only checked for generic variadic/remainder tokens (`(*)`, `(-)*`) and not whether the exact default entry (`*::: :->{name}`) had already been added. Repeated invocations led to duplicated rest specs and the `_arguments` error above. Fix: - Add a per-function guard variable `{prefix}_defaults_added`, initialized alongside `{prefix}_options` and set to `1` after the first append. This guarantees that the default `'*::: :->{name}'` is added at most once per session. - Keep (and extend) the presence checks: only append the defaults if neither a variadic/rest token (`(*)`, `(-)*`) nor the exact default entry is already present. Impact: - Eliminates duplicated `*::: :->…` entries and the “doubled rest argument definition” error in zsh, including across multiple invocations and after re-sourcing the file. - Generated zsh output is unchanged except for the guard variable lines.
1 parent acc76fd commit 9b5435f

File tree

2 files changed

+12
-3
lines changed

2 files changed

+12
-3
lines changed

shtab/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,10 +606,14 @@ def command_case(prefix, options):
606606

607607
return f"""\
608608
{prefix}() {{
609-
local context state line curcontext="$curcontext" one_or_more='(*)' remainder='(-)*'
609+
local context state line curcontext="$curcontext" one_or_more='(*)' remainder='(-)*' default='*::: :->{name}'
610610
611-
if ((${{{prefix}_options[(I)${{(q)one_or_more}}*]}} + ${{{prefix}_options[(I)${{(q)remainder}}*]}} == 0)); then # noqa: E501
612-
{prefix}_options+=(': :{prefix}_commands' '*::: :->{name}')
611+
# Add default positional/remainder specs only if none exist, and only once per session
612+
if (( ! {prefix}_defaults_added )); then
613+
if (( ${{{prefix}_options[(I)${{(q)one_or_more}}*]}} + ${{{prefix}_options[(I)${{(q)remainder}}*]}} + ${{{prefix}_options[(I)${{(q)default}}]}} == 0 )); then
614+
{prefix}_options+=(': :{prefix}_commands' '*::: :->{name}')
615+
fi
616+
{prefix}_defaults_added=1
613617
fi
614618
_arguments -C -s ${prefix}_options
615619
@@ -631,6 +635,9 @@ def command_option(prefix, options):
631635
{prefix}_options=(
632636
{arguments}
633637
)
638+
639+
# guard to ensure default positional specs are added only once per session
640+
{prefix}_defaults_added=0
634641
"""
635642

636643
def command_list(prefix, options):

tests/test_shtab.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ def test_prog_scripts(shell, caplog, capsys):
133133
elif shell == "zsh":
134134
assert script_py == [
135135
"#compdef script.py", "_describe 'script.py commands' _commands",
136+
'local context state line curcontext="$curcontext" '
137+
"one_or_more='(*)' remainder='(-)*' default='*::: :->script.py'",
136138
"_shtab_shtab_options+=(': :_shtab_shtab_commands' '*::: :->script.py')", "script.py)",
137139
"compdef _shtab_shtab -N script.py"]
138140
elif shell == "tcsh":

0 commit comments

Comments
 (0)