Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(completions): add quoting of command names #247

Merged
merged 3 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ test.py
/test
.pytest_cache
.vscode
*.patch
20 changes: 12 additions & 8 deletions src/cleo/commands/completions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
neersighted marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion tests/commands/completion/fixtures/bash.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ _my_function()
opts="${opts} "
;;

('spaced command')
opts="${opts} "
;;

esac

COMPREPLY=($(compgen -W "${opts}" -- ${cur}))
Expand All @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions tests/commands/completion/fixtures/command_with_space_in_name.py
Original file line number Diff line number Diff line change
@@ -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")]
5 changes: 4 additions & 1 deletion tests/commands/completion/fixtures/fish.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -34,3 +35,5 @@ complete -c script -A -n '__fish_seen_subcommand_from hello' -l option-without-d
# help

# list

# 'spaced command'
6 changes: 5 additions & 1 deletion tests/commands/completion/fixtures/zsh.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,6 +47,10 @@ _my_function()
opts+=()
;;

('spaced command')
opts+=()
;;

esac

_describe 'option' opts
Expand Down
6 changes: 6 additions & 0 deletions tests/commands/completion/test_completions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -19,6 +21,7 @@
app = Application()
app.add(HelloCommand())
app.add(CommandWithColons())
app.add(SpacedCommand())


def test_invalid_shell() -> None:
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down