Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Zsh completions with colons #2846

Open
wants to merge 1 commit into
base: release-8.2.0
Choose a base branch
from
Open
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
20 changes: 19 additions & 1 deletion src/click/shell_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def __getattr__(self, name: str) -> t.Any:
%(complete_func)s_setup;
"""

# See ZshComplete.format_completions below, and issue #2703, before
# changing this script.
_SOURCE_ZSH = """\
#compdef %(prog_name)s

Expand Down Expand Up @@ -366,7 +368,23 @@ def get_completion_args(self) -> tuple[list[str], str]:
return args, incomplete

def format_completion(self, item: CompletionItem) -> str:
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
""""""
# See issue #1812 and issue #2703 for context.
#
# Items *with* help are registered with zsh's `_describe`, which
# splits off the help/description after the first colon. So
# escape all colons in item.value so that they arrive intact.
#
# Items *without* help are registered with zsh's `compadd`,
# which does no colon handling. So do *not* escape colons in
# item.value, lest the completions themselves ultimately get
# mangled.
#
# Finally, there is no functional difference between using an
# empty help and using "_" as help.
help_ = item.help or "_"
value = item.value.replace(":", r"\:") if help_ != "_" else item.value
return f"{item.type}\n{value}\n{help_}"


class FishComplete(ShellComplete):
Expand Down
123 changes: 123 additions & 0 deletions tests/test_shell_completion.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import textwrap
import warnings

import pytest
Expand Down Expand Up @@ -324,6 +325,128 @@ def test_full_complete(runner, shell, env, expect):
assert result.output == expect


@pytest.mark.parametrize(
("env", "expect"),
[
(
{"COMP_WORDS": "", "COMP_CWORD": "0"},
textwrap.dedent(
"""\
plain
a
_
plain
b
bee
plain
c\\:d
cee:dee
plain
c:e
_
"""
),
),
(
{"COMP_WORDS": "a c", "COMP_CWORD": "1"},
textwrap.dedent(
"""\
plain
c\\:d
cee:dee
plain
c:e
_
"""
),
),
(
{"COMP_WORDS": "a c:", "COMP_CWORD": "1"},
textwrap.dedent(
"""\
plain
c\\:d
cee:dee
plain
c:e
_
"""
),
),
],
)
@pytest.mark.usefixtures("_patch_for_completion")
def test_zsh_full_complete_with_colons(runner, env, expect):
# See issue #2703 for context.
original_zsh_source_template = """\
#compdef %(prog_name)s

%(complete_func)s() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[%(prog_name)s] )) && return 1

response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
%(complete_var)s=zsh_complete %(prog_name)s)}")

for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
if [[ "$descr" == "_" ]]; then
completions+=("$key")
else
completions_with_descriptions+=("$key":"$descr")
fi
elif [[ "$type" == "dir" ]]; then
_path_files -/
elif [[ "$type" == "file" ]]; then
_path_files -f
fi
done

if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U
fi

if [ -n "$completions" ]; then
compadd -U -V unsorted -a completions
fi
}

if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
# autoload from fpath, call function directly
%(complete_func)s "$@"
else
# eval/source/. command, register function for later
compdef %(complete_func)s %(prog_name)s
fi
"""
complete_class = click.shell_completion.get_completion_class("zsh")
ZshComplete = click.shell_completion.ZshComplete
if (
complete_class != ZshComplete
and ZshComplete.source_template != original_zsh_source_template
):
pytest.fail(
"The Zsh source script has changed since click 8.2.0, "
"but the tests have not been updated to accomodate this. "
"See https://github.com/pallets/click/issues/2703 for "
"additional context."
)
cli = Group(
"cli",
commands=[
Command("a"),
Command("b", help="bee"),
Command("c:d", help="cee:dee"),
Command("c:e"),
],
)
env["_CLI_COMPLETE"] = "zsh_complete"
result = runner.invoke(cli, env=env)
assert result.output == expect


@pytest.mark.usefixtures("_patch_for_completion")
def test_context_settings(runner):
def complete(ctx, param, incomplete):
Expand Down
Loading