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
26 changes: 11 additions & 15 deletions src/click/shell_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,14 @@ def __getattr__(self, name: str) -> t.Any:
COMP_CWORD=(commandline -t) %(prog_name)s);

for completion in $response;
set -l metadata (string split \n $completion);
set -l metadata (string split "," $completion);

if test $metadata[1] = "dir";
__fish_complete_directories $metadata[2];
else if test $metadata[1] = "file";
__fish_complete_path $metadata[2];
else if test $metadata[1] = "plain";
if test $metadata[3] != "_";
echo $metadata[2]\t$metadata[3];
else;
echo $metadata[2];
end;
echo $metadata[2];
end;
end;
end;
Expand Down Expand Up @@ -427,15 +423,15 @@ def format_completion(self, item: CompletionItem) -> str:
Escape newlines in value and help to fix completion errors with
multi-line help strings.
"""
# The fish completion script splits each response line on literal
# newlines, so any newline in the value or help would corrupt the
# frame. Replace them with the two-character escape "\n" so the text
# round-trips through fish without breaking the format. The "_"
# sentinel for missing help mirrors :class:`ZshComplete`.
help_ = item.help or "_"
value = item.value.replace("\n", r"\n")
help_escaped = help_.replace("\n", r"\n")
return f"{item.type}\n{value}\n{help_escaped}"
# According to https://fishshell.com/docs/current/cmds/complete.html
# Command substitutions found in ARGUMENTS should return a newline-
# separated list of arguments, and each argument may optionally have a tab
# character followed by the argument description.
if item.help:
help_ = item.help.replace("\n", "\\n").replace("\t", " ")
return f"{item.type},{item.value}\t{help_}"

return f"{item.type},{item.value}"


ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")
Expand Down
53 changes: 3 additions & 50 deletions tests/test_shell_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,9 @@ def test_full_source(runner, shell):
("bash", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain,b\n"),
("zsh", {"COMP_WORDS": "", "COMP_CWORD": "0"}, "plain\na\n_\nplain\nb\nbee\n"),
("zsh", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain\nb\nbee\n"),
("fish", {"COMP_WORDS": "", "COMP_CWORD": ""}, "plain\na\n_\nplain\nb\nbee\n"),
("fish", {"COMP_WORDS": "a b", "COMP_CWORD": "b"}, "plain\nb\nbee\n"),
("fish", {"COMP_WORDS": 'a "b', "COMP_CWORD": '"b'}, "plain\nb\nbee\n"),
("fish", {"COMP_WORDS": "", "COMP_CWORD": ""}, "plain,a\nplain,b\tbee\n"),
("fish", {"COMP_WORDS": "a b", "COMP_CWORD": "b"}, "plain,b\tbee\n"),
("fish", {"COMP_WORDS": 'a "b', "COMP_CWORD": '"b'}, "plain,b\tbee\n"),
],
)
@pytest.mark.usefixtures("_patch_for_completion")
Expand Down Expand Up @@ -576,50 +576,3 @@ def cli(ctx, config_file):
assert not current_warnings, "There should be no warnings to start"
_get_completions(cli, args=[], incomplete="")
assert not current_warnings, "There should be no warnings after either"


@pytest.mark.usefixtures("_patch_for_completion")
def test_fish_multiline_help_complete(runner):
"""Test Fish completion with multi-line help text doesn't cause errors."""
cli = Command(
"cli",
params=[
Option(
["--at", "--attachment-type"],
type=(str, str),
multiple=True,
help=(
"\b\nAttachment with explicit mimetype,\n--at image.jpg image/jpeg"
),
),
Option(["--other"], help="Normal help"),
],
)

result = runner.invoke(
cli,
env={
"COMP_WORDS": "cli --",
"COMP_CWORD": "--",
"_CLI_COMPLETE": "fish_complete",
},
)

# Should not fail
assert result.exit_code == 0

# Output should contain escaped newlines, not literal newlines
# Fish expects: plain\n--at\n{help_with_\\n}
lines = result.output.split("\n")

# Find the --at completion block (3 lines: type, value, help)
for i in range(0, len(lines) - 2, 3):
if lines[i] == "plain" and lines[i + 1] in ("--at", "--attachment-type"):
help_line = lines[i + 2]
# Help should have escaped newlines (\\n), not actual newlines
assert "\\n" in help_line
# Should contain the example text
assert "image.jpg" in help_line.replace("\\n", " ")
break
else:
pytest.fail("--at completion not found in output")
Loading