Skip to content

Commit 648d7c4

Browse files
authored
Backport #3504 from main to stable branch (#3508)
2 parents 5b1aba8 + ddf20d0 commit 648d7c4

3 files changed

Lines changed: 29 additions & 66 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Version 8.4.2
55

66
Unreleased
77

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

914
Version 8.4.1
1015
-------------

src/click/shell_completion.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,14 @@ def __getattr__(self, name: str) -> t.Any:
181181
COMP_CWORD=(commandline -t) %(prog_name)s);
182182
183183
for completion in $response;
184-
set -l metadata (string split \n $completion);
184+
set -l metadata (string split "," $completion);
185185
186186
if test $metadata[1] = "dir";
187187
__fish_complete_directories $metadata[2];
188188
else if test $metadata[1] = "file";
189189
__fish_complete_path $metadata[2];
190190
else if test $metadata[1] = "plain";
191-
if test $metadata[3] != "_";
192-
echo $metadata[2]\t$metadata[3];
193-
else;
194-
echo $metadata[2];
195-
end;
191+
echo $metadata[2];
196192
end;
197193
end;
198194
end;
@@ -423,19 +419,19 @@ def get_completion_args(self) -> tuple[list[str], str]:
423419

424420
def format_completion(self, item: CompletionItem) -> str:
425421
"""
426-
.. versionchanged:: 8.4.0
427-
Escape newlines in value and help to fix completion errors with
428-
multi-line help strings.
422+
.. versionchanged:: 8.4.2
423+
Escape newlines and replace tabs with spaces in the help text to
424+
fix completion errors with multi-line help strings.
429425
"""
430-
# The fish completion script splits each response line on literal
431-
# newlines, so any newline in the value or help would corrupt the
432-
# frame. Replace them with the two-character escape "\n" so the text
433-
# round-trips through fish without breaking the format. The "_"
434-
# sentinel for missing help mirrors :class:`ZshComplete`.
435-
help_ = item.help or "_"
436-
value = item.value.replace("\n", r"\n")
437-
help_escaped = help_.replace("\n", r"\n")
438-
return f"{item.type}\n{value}\n{help_escaped}"
426+
# According to https://fishshell.com/docs/current/cmds/complete.html
427+
# Command substitutions found in ARGUMENTS should return a newline-
428+
# separated list of arguments, and each argument may optionally have a tab
429+
# character followed by the argument description.
430+
if item.help:
431+
help_ = item.help.replace("\n", "\\n").replace("\t", " ")
432+
return f"{item.type},{item.value}\t{help_}"
433+
434+
return f"{item.type},{item.value}"
439435

440436

441437
ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")

tests/test_shell_completion.py

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from click.core import Option
1313
from click.shell_completion import add_completion_class
1414
from click.shell_completion import CompletionItem
15+
from click.shell_completion import FishComplete
1516
from click.shell_completion import shell_complete
1617
from click.shell_completion import ShellComplete
1718
from click.types import Choice
@@ -359,9 +360,9 @@ def test_full_source(runner, shell):
359360
("bash", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain,b\n"),
360361
("zsh", {"COMP_WORDS": "", "COMP_CWORD": "0"}, "plain\na\n_\nplain\nb\nbee\n"),
361362
("zsh", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain\nb\nbee\n"),
362-
("fish", {"COMP_WORDS": "", "COMP_CWORD": ""}, "plain\na\n_\nplain\nb\nbee\n"),
363-
("fish", {"COMP_WORDS": "a b", "COMP_CWORD": "b"}, "plain\nb\nbee\n"),
364-
("fish", {"COMP_WORDS": 'a "b', "COMP_CWORD": '"b'}, "plain\nb\nbee\n"),
363+
("fish", {"COMP_WORDS": "", "COMP_CWORD": ""}, "plain,a\nplain,b\tbee\n"),
364+
("fish", {"COMP_WORDS": "a b", "COMP_CWORD": "b"}, "plain,b\tbee\n"),
365+
("fish", {"COMP_WORDS": 'a "b', "COMP_CWORD": '"b'}, "plain,b\tbee\n"),
365366
],
366367
)
367368
@pytest.mark.usefixtures("_patch_for_completion")
@@ -578,48 +579,9 @@ def cli(ctx, config_file):
578579
assert not current_warnings, "There should be no warnings after either"
579580

580581

581-
@pytest.mark.usefixtures("_patch_for_completion")
582-
def test_fish_multiline_help_complete(runner):
583-
"""Test Fish completion with multi-line help text doesn't cause errors."""
584-
cli = Command(
585-
"cli",
586-
params=[
587-
Option(
588-
["--at", "--attachment-type"],
589-
type=(str, str),
590-
multiple=True,
591-
help=(
592-
"\b\nAttachment with explicit mimetype,\n--at image.jpg image/jpeg"
593-
),
594-
),
595-
Option(["--other"], help="Normal help"),
596-
],
597-
)
598-
599-
result = runner.invoke(
600-
cli,
601-
env={
602-
"COMP_WORDS": "cli --",
603-
"COMP_CWORD": "--",
604-
"_CLI_COMPLETE": "fish_complete",
605-
},
606-
)
607-
608-
# Should not fail
609-
assert result.exit_code == 0
610-
611-
# Output should contain escaped newlines, not literal newlines
612-
# Fish expects: plain\n--at\n{help_with_\\n}
613-
lines = result.output.split("\n")
614-
615-
# Find the --at completion block (3 lines: type, value, help)
616-
for i in range(0, len(lines) - 2, 3):
617-
if lines[i] == "plain" and lines[i + 1] in ("--at", "--attachment-type"):
618-
help_line = lines[i + 2]
619-
# Help should have escaped newlines (\\n), not actual newlines
620-
assert "\\n" in help_line
621-
# Should contain the example text
622-
assert "image.jpg" in help_line.replace("\\n", " ")
623-
break
624-
else:
625-
pytest.fail("--at completion not found in output")
582+
def test_fish_format_completion_escapes_help():
583+
fc = FishComplete(Command("x"), {}, "x", "_X_COMPLETE")
584+
item = CompletionItem("--at", help="first\nsecond\tthird")
585+
# The newline is escaped to the literal characters backslash-n and the tab
586+
# becomes a space, so each completion stays on one line for fish.
587+
assert fc.format_completion(item) == "plain,--at\tfirst\\nsecond third"

0 commit comments

Comments
 (0)