diff --git a/.gitignore b/.gitignore index fe5789a8..1b161fef 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ test.py /test .pytest_cache .vscode +*.patch diff --git a/src/cleo/commands/completions_command.py b/src/cleo/commands/completions_command.py index ab73aba7..7d67fa0a 100644 --- a/src/cleo/commands/completions_command.py +++ b/src/cleo/commands/completions_command.py @@ -8,6 +8,7 @@ import subprocess from cleo import helpers +from cleo._compat import shell_quote from cleo.commands.command import Command from cleo.commands.completions.templates import TEMPLATES @@ -156,13 +157,14 @@ def render_bash(self) -> str: for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not cmd.enabled or not cmd.name: continue - cmds.append(cmd.name) + command_name = shell_quote(cmd.name) if " " in cmd.name else cmd.name + cmds.append(command_name) options = " ".join( f"--{opt.name}".replace(":", "\\:") for opt in sorted(cmd.definition.options, key=lambda o: o.name) ) cmds_opts += [ - f" ({cmd.name})", + f" ({command_name})", f' opts="${{opts}} {options}"', " ;;", "", # newline @@ -200,13 +202,14 @@ def sanitize(s: str) -> str: for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not cmd.enabled or not cmd.name: continue - cmds.append(self._zsh_describe(cmd.name, sanitize(cmd.description))) + command_name = shell_quote(cmd.name) if " " in cmd.name else cmd.name + cmds.append(self._zsh_describe(command_name, sanitize(cmd.description))) options = " ".join( self._zsh_describe(f"--{opt.name}", sanitize(opt.description)) for opt in sorted(cmd.definition.options, key=lambda o: o.name) ) cmds_opts += [ - f" ({cmd.name})", + f" ({command_name})", f" opts+=({options})", " ;;", "", # newline @@ -243,21 +246,22 @@ def sanitize(s: str) -> str: for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not cmd.enabled or not cmd.name: continue + command_name = shell_quote(cmd.name) if " " in cmd.name else cmd.name cmds.append( f"complete -c {script_name} -f -n '__fish{function}_no_subcommand' " - f"-a {cmd.name} -d '{sanitize(cmd.description)}'" + f"-a {command_name} -d '{sanitize(cmd.description)}'" ) cmds_opts += [ - f"# {cmd.name}", + f"# {command_name}", *[ f"complete -c {script_name} -A " - f"-n '__fish_seen_subcommand_from {cmd.name}' " + f"-n '__fish_seen_subcommand_from {command_name}' " f"-l {opt.name} -d '{sanitize(opt.description)}'" for opt in sorted(cmd.definition.options, key=lambda o: o.name) ], "", # newline ] - cmds_names.append(cmd.name) + cmds_names.append(command_name) return TEMPLATES["fish"] % { "script_name": script_name, diff --git a/tests/commands/completion/fixtures/bash.txt b/tests/commands/completion/fixtures/bash.txt index 317d1257..b42714e1 100644 --- a/tests/commands/completion/fixtures/bash.txt +++ b/tests/commands/completion/fixtures/bash.txt @@ -41,6 +41,10 @@ _my_function() opts="${opts} " ;; + ('spaced command') + opts="${opts} " + ;; + esac COMPREPLY=($(compgen -W "${opts}" -- ${cur})) @@ -51,7 +55,7 @@ _my_function() # completing for a command if [[ $cur == $com ]]; then - coms="command:with:colons hello help list" + coms="command:with:colons hello help list 'spaced command'" COMPREPLY=($(compgen -W "${coms}" -- ${cur})) __ltrim_colon_completions "$cur" diff --git a/tests/commands/completion/fixtures/command_with_space_in_name.py b/tests/commands/completion/fixtures/command_with_space_in_name.py new file mode 100644 index 00000000..aeba339b --- /dev/null +++ b/tests/commands/completion/fixtures/command_with_space_in_name.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from cleo.commands.command import Command +from cleo.helpers import argument + + +class SpacedCommand(Command): + name = "spaced command" + description = "Command with space in name." + arguments = [argument("test", description="test argument")] diff --git a/tests/commands/completion/fixtures/fish.txt b/tests/commands/completion/fixtures/fish.txt index 5d1879e1..57c02809 100644 --- a/tests/commands/completion/fixtures/fish.txt +++ b/tests/commands/completion/fixtures/fish.txt @@ -1,6 +1,6 @@ function __fish_my_function_no_subcommand for i in (commandline -opc) - if contains -- $i command:with:colons hello help list + if contains -- $i command:with:colons hello help list 'spaced command' return 1 end end @@ -21,6 +21,7 @@ complete -c script -f -n '__fish_my_function_no_subcommand' -a command:with:colo complete -c script -f -n '__fish_my_function_no_subcommand' -a hello -d 'Complete me please.' complete -c script -f -n '__fish_my_function_no_subcommand' -a help -d 'Displays help for a command.' complete -c script -f -n '__fish_my_function_no_subcommand' -a list -d 'Lists commands.' +complete -c script -f -n '__fish_my_function_no_subcommand' -a 'spaced command' -d 'Command with space in name.' # command options @@ -34,3 +35,5 @@ complete -c script -A -n '__fish_seen_subcommand_from hello' -l option-without-d # help # list + +# 'spaced command' diff --git a/tests/commands/completion/fixtures/zsh.txt b/tests/commands/completion/fixtures/zsh.txt index 5acf0df0..cf133ce6 100644 --- a/tests/commands/completion/fixtures/zsh.txt +++ b/tests/commands/completion/fixtures/zsh.txt @@ -21,7 +21,7 @@ _my_function() opts+=("--ansi:Force ANSI output." "--help:Display help for the given command. When no command is given display help for the list command." "--no-ansi:Disable ANSI output." "--no-interaction:Do not ask any interactive question." "--quiet:Do not output any message." "--verbose:Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug." "--version:Display this application version.") elif [[ $cur == $com ]]; then state="command" - coms+=("command\:with\:colons:Test." "hello:Complete me please." "help:Displays help for a command." "list:Lists commands.") + coms+=("command\:with\:colons:Test." "hello:Complete me please." "help:Displays help for a command." "list:Lists commands." "'spaced command':Command with space in name.") fi case $state in @@ -47,6 +47,10 @@ _my_function() opts+=() ;; + ('spaced command') + opts+=() + ;; + esac _describe 'option' opts diff --git a/tests/commands/completion/test_completions_command.py b/tests/commands/completion/test_completions_command.py index fb239407..190326a2 100644 --- a/tests/commands/completion/test_completions_command.py +++ b/tests/commands/completion/test_completions_command.py @@ -6,9 +6,11 @@ import pytest +from cleo._compat import WINDOWS from cleo.application import Application 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 from tests.commands.completion.fixtures.hello_command import HelloCommand @@ -19,6 +21,7 @@ app = Application() app.add(HelloCommand()) app.add(CommandWithColons()) +app.add(SpacedCommand()) def test_invalid_shell() -> None: @@ -29,6 +32,7 @@ def test_invalid_shell() -> None: tester.execute("pomodoro") +@pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_bash(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name", @@ -50,6 +54,7 @@ def test_bash(mocker: MockerFixture) -> None: assert expected == tester.io.fetch_output().replace("\r\n", "\n") +@pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_zsh(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name", @@ -71,6 +76,7 @@ def test_zsh(mocker: MockerFixture) -> None: assert expected == tester.io.fetch_output().replace("\r\n", "\n") +@pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_fish(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name",