Skip to content

Commit

Permalink
Various fixes to completion system
Browse files Browse the repository at this point in the history
Co-authored-by: Bartek Sokorski <b.sokorski@gmail.com>
Co-authored-by: Jürn Brodersen <juern.brodersen@googlemail.com>
  • Loading branch information
3 people authored Oct 25, 2023
1 parent d4ca6b1 commit f2b8075
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 21 deletions.
1 change: 1 addition & 0 deletions news/357.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed subcommand completions for Fish.
80 changes: 66 additions & 14 deletions src/cleo/commands/completions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import ClassVar
from typing import cast

from cleo import helpers
from cleo._compat import shell_quote
from cleo.commands.command import Command
from cleo.commands.completions.templates import TEMPLATES
from cleo.exceptions import CleoRuntimeError


if TYPE_CHECKING:
Expand Down Expand Up @@ -138,10 +140,32 @@ def render(self, shell: str) -> str:

raise RuntimeError(f"Unrecognized shell: {shell}")

@staticmethod
def _get_prog_name_from_stack() -> str:
package_name = ""
frame = inspect.currentframe()
f_back = frame.f_back if frame is not None else None
f_globals = f_back.f_globals if f_back is not None else None
# break reference cycle
# https://docs.python.org/3/library/inspect.html#the-interpreter-stack
del frame

if f_globals is not None:
package_name = cast(str, f_globals.get("__name__"))

if package_name == "__main__":
package_name = cast(str, f_globals.get("__package__"))

if package_name:
package_name = package_name.partition(".")[0]

if not package_name:
raise CleoRuntimeError("Can not determine package name")

return package_name

def _get_script_name_and_path(self) -> tuple[str, str]:
# FIXME: when generating completions via `python -m script completions`,
# we incorrectly infer `script_name` as `__main__.py`
script_name = self._io.input.script_name or inspect.stack()[-1][1]
script_name = self._io.input.script_name or self._get_prog_name_from_stack()
script_path = posixpath.realpath(script_name)
script_name = Path(script_path).name

Expand Down Expand Up @@ -250,34 +274,62 @@ def sanitize(s: str) -> str:
# Commands + options
cmds = []
cmds_opts = []
cmds_names = []
namespaces = set()
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 {command_name} -d '{sanitize(cmd.description)}'"
)
cmd_path = cmd.name.split(" ")
namespace = cmd_path[0]
cmd_name = cmd_path[-1] if " " in cmd.name else cmd.name

# We either have a command like `poetry add` or a nested (namespaced)
# command like `poetry cache clear`.
if len(cmd_path) == 1:
cmds.append(
f"complete -c {script_name} -f -n '__fish{function}_no_subcommand' "
f"-a {cmd_name} -d '{sanitize(cmd.description)}'"
)
condition = f"__fish_seen_subcommand_from {cmd_name}"
else:
# Complete the namespace first
if namespace not in namespaces:
cmds.append(
f"complete -c {script_name} -f -n "
f"'__fish{function}_no_subcommand' -a {namespace}"
)
# Now complete the command
subcmds = [
name.split(" ")[-1] for name in self.application.all(namespace)
]
cmds.append(
f"complete -c {script_name} -f -n '__fish_seen_subcommand_from "
f"{namespace}; and not __fish_seen_subcommand_from {' '.join(subcmds)}' "
f"-a {cmd_name} -d '{sanitize(cmd.description)}'"
)
condition = (
f"__fish_seen_subcommand_from {namespace}; "
f"and __fish_seen_subcommand_from {cmd_name}"
)

cmds_opts += [
f"# {command_name}",
f"# {cmd.name}",
*[
f"complete -c {script_name} -A "
f"-n '__fish_seen_subcommand_from {sanitize(command_name)}' "
f"complete -c {script_name} "
f"-n '{condition}' "
f"-l {opt.name} -d '{sanitize(opt.description)}'"
for opt in sorted(cmd.definition.options, key=lambda o: o.name)
],
"", # newline
]
cmds_names.append(command_name)
namespaces.add(namespace)

return TEMPLATES["fish"] % {
"script_name": script_name,
"function": function,
"opts": "\n".join(opts),
"cmds": "\n".join(cmds),
"cmds_opts": "\n".join(cmds_opts[:-1]), # trim trailing newline
"cmds_names": " ".join(cmds_names),
"cmds_names": " ".join(sorted(namespaces)),
}

def get_shell_type(self) -> str:
Expand Down
15 changes: 8 additions & 7 deletions 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 'spaced command'
if contains -- $i command:with:colons hello help list spaced
return 1
end
end
Expand All @@ -21,20 +21,21 @@ 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.'
complete -c script -f -n '__fish_my_function_no_subcommand' -a spaced
complete -c script -f -n '__fish_seen_subcommand_from spaced; and not __fish_seen_subcommand_from command' -a command -d 'Command with space in name.'

# command options

# command:with:colons
complete -c script -A -n '__fish_seen_subcommand_from command:with:colons' -l goodbye -d ''
complete -c script -n '__fish_seen_subcommand_from command:with:colons' -l goodbye -d ''

# hello
complete -c script -A -n '__fish_seen_subcommand_from hello' -l dangerous-option -d 'This $hould be `escaped`.'
complete -c script -A -n '__fish_seen_subcommand_from hello' -l option-without-description -d ''
complete -c script -n '__fish_seen_subcommand_from hello' -l dangerous-option -d 'This $hould be `escaped`.'
complete -c script -n '__fish_seen_subcommand_from hello' -l option-without-description -d ''

# help

# list

# 'spaced command'
complete -c script -A -n '__fish_seen_subcommand_from \'spaced command\'' -l goodbye -d ''
# spaced command
complete -c script -n '__fish_seen_subcommand_from spaced; and __fish_seen_subcommand_from command' -l goodbye -d ''

0 comments on commit f2b8075

Please sign in to comment.