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
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Version 8.4.2

Unreleased

- Fix Fish shell completion broken in ``8.4.0`` by :pr:`3126`. Newlines and
tabs in option help text are now escaped, keeping the original completion
format while still supporting multi-line help. :issue:`3502`
:issue:`3043` :pr:`3504` :pr:`3508`


Version 8.4.1
-------------
Expand Down
32 changes: 14 additions & 18 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 @@ -423,19 +419,19 @@ def get_completion_args(self) -> tuple[list[str], str]:

def format_completion(self, item: CompletionItem) -> str:
"""
.. versionchanged:: 8.4.0
Escape newlines in value and help to fix completion errors with
multi-line help strings.
.. versionchanged:: 8.4.2
Escape newlines and replace tabs with spaces in the help text 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
58 changes: 10 additions & 48 deletions tests/test_shell_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from click.core import Option
from click.shell_completion import add_completion_class
from click.shell_completion import CompletionItem
from click.shell_completion import FishComplete
from click.shell_completion import shell_complete
from click.shell_completion import ShellComplete
from click.types import Choice
Expand Down Expand Up @@ -359,9 +360,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 @@ -578,48 +579,9 @@ def cli(ctx, config_file):
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")
def test_fish_format_completion_escapes_help():
fc = FishComplete(Command("x"), {}, "x", "_X_COMPLETE")
item = CompletionItem("--at", help="first\nsecond\tthird")
# The newline is escaped to the literal characters backslash-n and the tab
# becomes a space, so each completion stays on one line for fish.
assert fc.format_completion(item) == "plain,--at\tfirst\\nsecond third"