From 46f46e0830ea2f0275e780c540d74d85473aad7f Mon Sep 17 00:00:00 2001 From: Frantisek Tobias Date: Mon, 7 Oct 2024 13:51:46 +0200 Subject: [PATCH] kresctl: tab-completion: implement suggestions/completion for first argument --- python/knot_resolver/client/commands/cache.py | 12 +- .../client/commands/completion.py | 106 ++++++++++++------ 2 files changed, 81 insertions(+), 37 deletions(-) diff --git a/python/knot_resolver/client/commands/cache.py b/python/knot_resolver/client/commands/cache.py index 60417eec5..ef64302b3 100644 --- a/python/knot_resolver/client/commands/cache.py +++ b/python/knot_resolver/client/commands/cache.py @@ -8,6 +8,7 @@ from knot_resolver.utils.modeling.exceptions import AggregateDataValidationError, DataValidationError from knot_resolver.utils.modeling.parsing import DataFormat, parse_json from knot_resolver.utils.requests import request +from argparse import _SubParsersAction class CacheOperations(Enum): @@ -99,7 +100,16 @@ def register_args_subparser( @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - return {} + words = dict() + for action in parser._actions: + if isinstance(action, _SubParsersAction): + if action.choices is not None: + for choice in action.choices: + words[choice] = action.choices.get(choice) + else: + for opt in action.option_strings: + words[opt] = action.help + return words def run(self, args: CommandArgs) -> None: if not self.operation: diff --git a/python/knot_resolver/client/commands/completion.py b/python/knot_resolver/client/commands/completion.py index 05fdded82..23871afd7 100644 --- a/python/knot_resolver/client/commands/completion.py +++ b/python/knot_resolver/client/commands/completion.py @@ -3,6 +3,7 @@ from typing import List, Tuple, Type from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command +from argparse import _SubParsersAction class Shells(Enum): @@ -10,6 +11,38 @@ class Shells(Enum): FISH = 1 +def parser_words(actions): + words = dict() + for action in actions: + if isinstance(action, _SubParsersAction): + if action.choices is not None: + for choice in action.choices: + words[choice] = action.choices.get(choice) + else: + for opt in action.option_strings: + words[opt] = action.help + + return words + + +def subparser_by_name(uarg: str, actions) -> argparse.ArgumentParser | None: + for action in actions: + if isinstance(action, _SubParsersAction): + if action.choices is not None: + for choice in action.choices: + # if uarg == choice: + if uarg in choice: + return action.choices.get(choice) + return None + + +def subparser_command(subparser: argparse.ArgumentParser) -> Command: + com_class: Command | None = subparser._defaults.get("command") + # NOTE: This is just a temporary bandage to silence pyright + assert(com_class is not None) + return com_class + + @register_command class CompletionCommand(Command): def __init__(self, namespace: argparse.Namespace) -> None: @@ -57,39 +90,40 @@ def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: return words def run(self, args: CommandArgs) -> None: - pass - # subparsers = args.parser._subparsers - # words: CompWords = {} - - # if subparsers: - # words = parser_words(subparsers._actions) - - # uargs = iter(self.comp_args) - # for uarg in uargs: - # subparser = subparser_by_name(uarg, subparsers._actions) # pylint: disable=W0212 - - # if subparser: - # cmd: Command = subparser_command(subparser) - # subparser_args = self.comp_args[self.comp_args.index(uarg) + 1 :] - # if subparser_args: - # words = cmd.completion(subparser_args, subparser) - # break - # elif uarg in ["-s", "--socket"]: - # # if arg is socket config, skip next arg - # next(uargs) - # continue - # elif uarg in words: - # # uarg is walid arg, continue - # continue - # else: - # raise ValueError(f"unknown argument: {uarg}") - - # # print completion words - # # based on required bash/fish shell format - # if self.shell == Shells.BASH: - # print(" ".join(words)) - # elif self.shell == Shells.FISH: - # # TODO: FISH completion implementation - # pass - # else: - # raise ValueError(f"unexpected value of {Shells}: {self.shell}") + subparsers = args.parser._subparsers + words: CompWords = {} + + if subparsers: + words = parser_words(subparsers._actions) + + uargs = iter(self.comp_args) + # skip kresctl + next(uargs) + for uarg in uargs: + subparser = subparser_by_name(uarg, subparsers._actions) # pylint: disable=W0212 + + if subparser: + cmd: Command = subparser_command(subparser) + subparser_args = self.comp_args[self.comp_args.index(uarg) + 1 :] + if subparser_args: + words = cmd.completion(subparser_args, subparser) + break + elif uarg in ["-s", "--socket"]: + # if arg is socket config, skip next arg + next(uargs) + continue + elif uarg in words: + # uarg is walid arg, continue + continue + else: + raise ValueError(f"unknown argument: {uarg}") + + # print completion words + # based on required bash/fish shell format + if self.shell == Shells.BASH: + print(" ".join(words)) + elif self.shell == Shells.FISH: + # TODO: FISH completion implementation + pass + else: + raise ValueError(f"unexpected value of {Shells}: {self.shell}")