diff --git a/news/357.bugfix.md b/news/357.bugfix.md new file mode 100644 index 00000000..3efdd799 --- /dev/null +++ b/news/357.bugfix.md @@ -0,0 +1 @@ +Fixed subcommand completions for Fish. diff --git a/src/cleo/commands/completions_command.py b/src/cleo/commands/completions_command.py index 6a9df744..3c9d422b 100644 --- a/src/cleo/commands/completions_command.py +++ b/src/cleo/commands/completions_command.py @@ -249,26 +249,54 @@ 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)}' " # noqa: E501 + 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"-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, @@ -276,7 +304,7 @@ def sanitize(s: str) -> str: "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: diff --git a/tests/commands/completion/fixtures/fish.txt b/tests/commands/completion/fixtures/fish.txt index e47611d3..07a991bc 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 'spaced command' + if contains -- $i command:with:colons hello help list spaced return 1 end end @@ -21,7 +21,8 @@ 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 @@ -36,5 +37,5 @@ complete -c script -A -n '__fish_seen_subcommand_from hello' -l option-without-d # list -# 'spaced command' -complete -c script -A -n '__fish_seen_subcommand_from \'spaced command\'' -l goodbye -d '' +# spaced command +complete -c script -A -n '__fish_seen_subcommand_from spaced; and __fish_seen_subcommand_from command' -l goodbye -d ''