summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleš Mrázek <ales.mrazek@nic.cz>2024-12-16 18:04:47 +0100
committerAleš Mrázek <ales.mrazek@nic.cz>2024-12-20 22:24:22 +0100
commit06f06caa559b46931b8be63bffe8bd79baf0b483 (patch)
tree5cf5c11947e00dec8f21e0951ab28b2ab8dcfdc0
parentpython: client: completion: improved top-level behavior (diff)
downloadknot-resolver-06f06caa559b46931b8be63bffe8bd79baf0b483.tar.xz
knot-resolver-06f06caa559b46931b8be63bffe8bd79baf0b483.zip
python: client: command: move the getting of completion words to function
- #dirnames# and #filenames# words to indicate that we want complete also files and dirs
-rw-r--r--python/knot_resolver/client/command.py95
-rw-r--r--python/knot_resolver/client/commands/cache.py4
-rw-r--r--python/knot_resolver/client/commands/completion.py67
-rw-r--r--python/knot_resolver/client/commands/config.py4
-rw-r--r--python/knot_resolver/client/commands/convert.py6
-rw-r--r--python/knot_resolver/client/commands/debug.py19
-rw-r--r--python/knot_resolver/client/commands/help.py4
-rw-r--r--python/knot_resolver/client/commands/metrics.py4
-rw-r--r--python/knot_resolver/client/commands/schema.py4
-rw-r--r--python/knot_resolver/client/commands/validate.py4
-rw-r--r--utils/shell-completion/client.bash27
11 files changed, 123 insertions, 115 deletions
diff --git a/python/knot_resolver/client/command.py b/python/knot_resolver/client/command.py
index 5a0d11a7..e4eddf08 100644
--- a/python/knot_resolver/client/command.py
+++ b/python/knot_resolver/client/command.py
@@ -1,7 +1,7 @@
import argparse
from abc import ABC, abstractmethod # pylint: disable=[no-name-in-module]
from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
+from typing import Dict, List, Optional, Tuple, Type, TypeVar
from urllib.parse import quote
from knot_resolver.constants import API_SOCK_FILE, CONFIG_FILE
@@ -14,33 +14,96 @@ T = TypeVar("T", bound=Type["Command"])
CompWords = Dict[str, Optional[str]]
+COMP_DIRNAMES = "#dirnames#"
+COMP_FILENAMES = "#filenames#"
+
_registered_commands: List[Type["Command"]] = []
-def get_subparsers_words(subparser_actions: List[argparse.Action]) -> CompWords:
+def get_parser_action(name: str, parser_actions: List[argparse.Action]) -> Optional[argparse.Action]:
+ for action in parser_actions:
+ if (action.choices and name in action.choices) or (action.option_strings and name in action.option_strings):
+ return action
+ return None
+
+
+def get_subparser_command(subparser: argparse.ArgumentParser) -> Optional["Command"]:
+ if "command" in subparser._defaults: # noqa: SLF001
+ return subparser._defaults["command"] # noqa: SLF001
+ return None
+
+
+def comp_get_actions_words(parser_actions: List[argparse.Action]) -> CompWords:
words: CompWords = {}
- for action in subparser_actions:
+ for action in parser_actions:
if isinstance(action, argparse._SubParsersAction) and action.choices: # noqa: SLF001
for choice, parser in action.choices.items():
- words[choice] = parser.description
- else:
+ words[choice] = parser.description if isinstance(parser, argparse.ArgumentParser) else None
+ elif action.option_strings:
for opt in action.option_strings:
words[opt] = action.help
+ elif not action.option_strings and action.choices:
+ for choice in action.choices:
+ words[choice] = action.help
+ elif not action.option_strings and not action.choices:
+ words[COMP_DIRNAMES] = None
+ words[COMP_FILENAMES] = None
return words
-def get_action_by_name(name: str, parser_actions: List[argparse.Action]) -> Optional[argparse.Action]:
- for action in parser_actions:
- if (action.choices and name in action.choices) or (action.option_strings and name in action.option_strings):
- return action
- return None
-
+def comp_get_words(args: List[str], parser_actions: List[argparse.Action]) -> CompWords: # noqa: PLR0912
+ words: CompWords = comp_get_actions_words(parser_actions)
+ nargs = len(args)
+
+ skip_arg = False
+ for i, arg in enumerate(args):
+ action: Optional[argparse.Action] = get_parser_action(arg, parser_actions) # noqa: SLF001
+
+ if skip_arg:
+ skip_arg = False
+ continue
+
+ if not action:
+ words = comp_get_actions_words(parser_actions)
+ continue
+
+ if i + 1 >= nargs:
+ return words
+
+ # if not action or action is HelpAction or VersionAction
+ if isinstance(action, (argparse._HelpAction, argparse._VersionAction)): # noqa: SLF001
+ words = {}
+ break
+
+ # if action is StoreTrueAction or StoreFalseAction
+ if isinstance(action, argparse._StoreConstAction): # noqa: SLF001
+ continue
+
+ # if action is StoreAction
+ if isinstance(action, argparse._StoreAction): # noqa: SLF001
+ choices = {}
+ if action.choices:
+ for choice in action.choices:
+ choices[choice] = action.help
+ else:
+ choices[COMP_DIRNAMES] = None
+ choices[COMP_FILENAMES] = None
+ words = choices
+ skip_arg = True
+ continue
+
+ # if action is SubParserAction
+ if isinstance(action, argparse._SubParsersAction): # noqa: SLF001
+ subparser = action.choices[arg]
+ cmd = get_subparser_command(subparser)
+ return cmd.completion(args[i + 1 :], subparser) if cmd else {}
+
+ # delete already used args
+ for arg in args:
+ if arg in words.keys():
+ del words[arg]
-def get_subparser_command(subparser: argparse.ArgumentParser) -> "Command":
- defaults: Dict[str, Any] = subparser._defaults # noqa: SLF001
- if "command" in defaults:
- return defaults["command"]
- raise ValueError(f"missing 'command' default for '{subparser.prog}' parser")
+ return words
def register_command(cls: T) -> T:
diff --git a/python/knot_resolver/client/commands/cache.py b/python/knot_resolver/client/commands/cache.py
index d1116580..66ee77c9 100644
--- a/python/knot_resolver/client/commands/cache.py
+++ b/python/knot_resolver/client/commands/cache.py
@@ -3,7 +3,7 @@ import sys
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.datamodel.cache_schema import CacheClearRPCSchema
from knot_resolver.utils.modeling.exceptions import AggregateDataValidationError, DataValidationError
from knot_resolver.utils.modeling.parsing import DataFormat, parse_json
@@ -99,7 +99,7 @@ class CacheCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
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 1da8c516..f3101c19 100644
--- a/python/knot_resolver/client/commands/completion.py
+++ b/python/knot_resolver/client/commands/completion.py
@@ -1,14 +1,12 @@
import argparse
from enum import Enum
-from typing import List, Optional, Tuple, Type
+from typing import List, Tuple, Type
from knot_resolver.client.command import (
Command,
CommandArgs,
CompWords,
- get_action_by_name,
- get_subparser_command,
- get_subparsers_words,
+ comp_get_words,
register_command,
)
@@ -23,12 +21,8 @@ class CompletionCommand(Command):
def __init__(self, namespace: argparse.Namespace) -> None:
super().__init__(namespace)
self.shell: Shells = namespace.shell
- self.space = namespace.space
self.args: List[str] = namespace.args
- if self.space:
- self.args.append("")
-
@staticmethod
def register_args_subparser(
subparser: "argparse._SubParsersAction[argparse.ArgumentParser]",
@@ -37,13 +31,6 @@ class CompletionCommand(Command):
"completion",
help="commands auto-completion",
)
- completion.add_argument(
- "--space",
- help="space after last word, returns all possible folowing options",
- dest="space",
- action="store_true",
- default=False,
- )
shells_dest = "shell"
shells = completion.add_mutually_exclusive_group()
@@ -56,58 +43,14 @@ class CompletionCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None: # noqa: PLR0912
- subparsers = args.parser._subparsers # noqa: SLF001
words: CompWords = {}
+ subparsers = args.parser._subparsers # noqa: SLF001
if subparsers:
- words = get_subparsers_words(subparsers._actions) # noqa: SLF001
-
- args_iter = iter(self.args)
- for arg in args_iter:
- action: Optional[argparse.Action] = get_action_by_name(arg, subparsers._actions) # noqa: SLF001
-
- # if action is SubParserAction; complete using the command
- if isinstance(action, argparse._SubParsersAction) and arg in action.choices: # noqa: SLF001
- # remove from words
- for choice in action.choices:
- del words[choice]
-
- subparser = action.choices[arg]
- cmd = get_subparser_command(subparser)
-
- nargs = len(self.args)
- index = self.args.index(arg) + 1
- # check that index is not out of args length
- if index > nargs:
- break
-
- # complete using the command
- words = cmd.completion(self.args[index:], subparser)
- break
-
- # if action is StoreAction; skip number of arguments
- if isinstance(action, argparse._StoreAction) and arg in action.option_strings: # noqa: SLF001
- # remove from words
- for option_string in action.option_strings:
- del words[option_string]
-
- if action.nargs and isinstance(action.nargs, int):
- for _ in range(action.nargs):
- next(args_iter)
- continue
-
- # remove other options from words
- if action and action.option_strings:
- for option_string in action.option_strings:
- del words[option_string]
-
- # if 'arg' is not found in actions
- # there is nothing to complete
- if not action:
- break
+ words = comp_get_words(self.args, subparsers._actions) # noqa: SLF001
# print completion words
# based on required bash/fish shell format
diff --git a/python/knot_resolver/client/commands/config.py b/python/knot_resolver/client/commands/config.py
index dbf6ccaa..3ebc96ec 100644
--- a/python/knot_resolver/client/commands/config.py
+++ b/python/knot_resolver/client/commands/config.py
@@ -3,7 +3,7 @@ import sys
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.datamodel import KresConfig
from knot_resolver.utils.modeling.parsing import DataFormat, parse_json, try_to_parse
from knot_resolver.utils.requests import request
@@ -170,7 +170,7 @@ class ConfigCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- words = get_subparsers_words(parser._actions) # noqa: SLF001
+ words = comp_get_words(args, parser._actions) # noqa: SLF001
if args is None:
return words
diff --git a/python/knot_resolver/client/commands/convert.py b/python/knot_resolver/client/commands/convert.py
index b72af758..28a5be95 100644
--- a/python/knot_resolver/client/commands/convert.py
+++ b/python/knot_resolver/client/commands/convert.py
@@ -3,7 +3,7 @@ import sys
from pathlib import Path
from typing import List, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.datamodel import KresConfig
from knot_resolver.datamodel.globals import Context, reset_global_validation_context, set_global_validation_context
from knot_resolver.utils.modeling import try_to_parse
@@ -39,7 +39,6 @@ class ConvertCommand(Command):
type=str,
help="File with configuration in YAML or JSON format.",
)
-
convert.add_argument(
"output_file",
type=str,
@@ -47,12 +46,11 @@ class ConvertCommand(Command):
help="Optional, output file for converted configuration in Lua script. If not specified, converted configuration is printed.",
default=None,
)
-
return convert, ConvertCommand
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None:
with open(self.input_file, "r") as f:
diff --git a/python/knot_resolver/client/commands/debug.py b/python/knot_resolver/client/commands/debug.py
index 5d9a81df..00853c03 100644
--- a/python/knot_resolver/client/commands/debug.py
+++ b/python/knot_resolver/client/commands/debug.py
@@ -5,7 +5,7 @@ import sys
from pathlib import Path
from typing import List, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.utils import which
from knot_resolver.utils.requests import request
@@ -31,13 +31,6 @@ class DebugCommand(Command):
help="Run GDB on the manager's subprocesses",
)
debug.add_argument(
- "proc_type",
- help="Optional, the type of process to debug. May be 'kresd' (default), 'gc', or 'all'.",
- type=str,
- nargs="?",
- default="kresd",
- )
- debug.add_argument(
"--sudo",
dest="sudo",
help="Run GDB with sudo",
@@ -56,11 +49,19 @@ class DebugCommand(Command):
action="store_true",
default=False,
)
+ debug.add_argument(
+ "proc_type",
+ help="Optional, the type of process to debug. May be 'kresd', 'gc', or 'all'.",
+ choices=["kresd", "gc", "all"],
+ type=str,
+ nargs="?",
+ default="kresd",
+ )
return debug, DebugCommand
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return {}
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None: # noqa: PLR0912, PLR0915
if self.gdb is None:
diff --git a/python/knot_resolver/client/commands/help.py b/python/knot_resolver/client/commands/help.py
index 87306c2a..1db83fb6 100644
--- a/python/knot_resolver/client/commands/help.py
+++ b/python/knot_resolver/client/commands/help.py
@@ -1,7 +1,7 @@
import argparse
from typing import List, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
@register_command
@@ -14,7 +14,7 @@ class HelpCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return {}
+ return comp_get_words(args, parser._actions) # noqa: SLF001
@staticmethod
def register_args_subparser(
diff --git a/python/knot_resolver/client/commands/metrics.py b/python/knot_resolver/client/commands/metrics.py
index 85cff258..eaf83090 100644
--- a/python/knot_resolver/client/commands/metrics.py
+++ b/python/knot_resolver/client/commands/metrics.py
@@ -2,7 +2,7 @@ import argparse
import sys
from typing import List, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.utils.modeling.parsing import DataFormat, parse_json
from knot_resolver.utils.requests import request
@@ -44,7 +44,7 @@ class MetricsCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None:
response = request(args.socket, "GET", "metrics/prometheus" if self.prometheus else "metrics/json")
diff --git a/python/knot_resolver/client/commands/schema.py b/python/knot_resolver/client/commands/schema.py
index fa7465c1..fdb65bde 100644
--- a/python/knot_resolver/client/commands/schema.py
+++ b/python/knot_resolver/client/commands/schema.py
@@ -3,7 +3,7 @@ import json
import sys
from typing import List, Optional, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.datamodel import kres_config_json_schema
from knot_resolver.utils.requests import request
@@ -35,7 +35,7 @@ class SchemaCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None:
if self.live:
diff --git a/python/knot_resolver/client/commands/validate.py b/python/knot_resolver/client/commands/validate.py
index 141a2003..2347519e 100644
--- a/python/knot_resolver/client/commands/validate.py
+++ b/python/knot_resolver/client/commands/validate.py
@@ -3,7 +3,7 @@ import sys
from pathlib import Path
from typing import List, Tuple, Type
-from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
from knot_resolver.datamodel import KresConfig
from knot_resolver.datamodel.globals import Context, reset_global_validation_context, set_global_validation_context
from knot_resolver.utils.modeling import try_to_parse
@@ -41,7 +41,7 @@ class ValidateCommand(Command):
@staticmethod
def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
- return get_subparsers_words(parser._actions) # noqa: SLF001
+ return comp_get_words(args, parser._actions) # noqa: SLF001
def run(self, args: CommandArgs) -> None:
if self.input_file:
diff --git a/utils/shell-completion/client.bash b/utils/shell-completion/client.bash
index a1c8290b..0df57f31 100644
--- a/utils/shell-completion/client.bash
+++ b/utils/shell-completion/client.bash
@@ -1,24 +1,27 @@
-#/usr/bin/env bash
+#!/usr/bin/env bash
_kresctl_completion()
{
COMPREPLY=()
+ local args=""
local words=""
- local space_arg=""
local cur="${COMP_WORDS[COMP_CWORD]}"
+ local opts=$(kresctl completion --bash --args "${COMP_WORDS[@]:1}")
- # if the current word is empty
- # we need to inform the kresctl client about it
- if [[ -z "$cur" ]]; then
- space_arg="--space"
+ # filter special opts
+ for opt in $opts
+ do
+ if [[ "$opt" == "#dirnames#" ]]; then
+ args="$args${args:+ }-d"
+ elif [[ "$opt" == "#filenames#" ]]; then
+ args="$args${args:+ }-f"
+ else
+ words="$words${words:+ }$opt"
fi
+ done
- # get words from the kresctl client
- words=$(kresctl completion --bash ${space_arg} --args "${COMP_WORDS[@]:1}")
-
- COMPREPLY=($(compgen -W "${words}" -- "${cur}"))
-
+ COMPREPLY=($(compgen $args -W "${words}" -- "${cur}"))
return 0
}
-complete -o filenames -o dirnames -o nosort -F _kresctl_completion kresctl
+complete -o nosort -F _kresctl_completion kresctl