Skip to content
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
1 change: 1 addition & 0 deletions news/536.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Escape backslashes in generated fish completion descriptions.
10 changes: 9 additions & 1 deletion src/cleo/commands/completions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,15 @@ def render_fish(self) -> str:
function = self._generate_function_name(script_name, script_path)

def sanitize(s: str) -> str:
return self._io.output.formatter.remove_format(s).replace("'", "\\'")
# Descriptions are emitted inside single-quoted fish arguments
# (`-d '...'`). In fish, a backslash inside single quotes still
# escapes a following backslash or single quote, so it has to be
# escaped before the single quote is.
return (
self._io.output.formatter.remove_format(s)
.replace("\\", "\\\\")
.replace("'", "\\'")
)

# Global options
assert self.application
Expand Down
31 changes: 31 additions & 0 deletions tests/commands/completion/test_completions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from cleo._compat import WINDOWS
from cleo.application import Application
from cleo.commands.command import Command
from cleo.testers.command_tester import CommandTester
from tests.commands.completion.fixtures.command_with_colons import CommandWithColons
from tests.commands.completion.fixtures.command_with_space_in_name import SpacedCommand
Expand Down Expand Up @@ -94,3 +95,33 @@ def test_fish(mocker: MockerFixture) -> None:
expected = (FIXTURES_PATH / "fish.txt").read_text(encoding="utf-8")

assert expected == tester.io.fetch_output().replace("\r\n", "\n")


@pytest.mark.skipif(WINDOWS, reason="Only test linux shells")
def test_fish_escapes_backslashes_in_descriptions(mocker: MockerFixture) -> None:
mocker.patch(
"cleo.io.inputs.string_input.StringInput.script_name",
new_callable=mocker.PropertyMock,
return_value="/path/to/my/script",
)
mocker.patch(
"cleo.commands.completions_command.CompletionsCommand._generate_function_name",
return_value="_my_function",
)

class BackslashCommand(Command):
name = "winpath"
description = "Use C:\\temp and it's fun"

local_app = Application()
local_app.add(BackslashCommand())

command = local_app.find("completions")
tester = CommandTester(command)
tester.execute("fish")
output = tester.io.fetch_output().replace("\r\n", "\n")

# The description is emitted inside `-d '...'`. A backslash inside fish
# single quotes still escapes the next character, so it must be doubled;
# otherwise the closing quote can be escaped and the script breaks.
assert "-d 'Use C:\\\\temp and it\\'s fun'" in output