diff options
author | Lukáš Ondráček <lukas.ondracek@nic.cz> | 2025-01-02 14:38:53 +0100 |
---|---|---|
committer | Lukáš Ondráček <lukas.ondracek@nic.cz> | 2025-01-02 14:38:53 +0100 |
commit | a01dbeba1852a6cd71e81fb99f65e0810ec0046f (patch) | |
tree | 2347fb0d57801f13279af9f4c1435def5af50990 | |
parent | daemon/defer: fix configuration reload (diff) | |
parent | Merge branch 'kresctl-tab-completion' into 'master' (diff) | |
download | knot-resolver-a01dbeba1852a6cd71e81fb99f65e0810ec0046f.tar.xz knot-resolver-a01dbeba1852a6cd71e81fb99f65e0810ec0046f.zip |
Merge branch 'master' into defer-wip
-rw-r--r-- | NEWS | 4 | ||||
-rw-r--r-- | daemon/engine.c | 2 | ||||
-rw-r--r-- | distro/pkg/deb/knot-resolver6.install | 1 | ||||
-rw-r--r-- | distro/pkg/rpm/knot-resolver.spec | 1 | ||||
-rw-r--r-- | lib/module.c | 6 | ||||
-rw-r--r-- | lib/module.h | 2 | ||||
-rw-r--r-- | modules/stats/stats.c | 3 | ||||
-rw-r--r-- | python/knot_resolver/client/command.py | 118 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/cache.py | 4 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/completion.py | 90 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/config.py | 110 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/convert.py | 6 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/debug.py | 21 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/help.py | 4 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/metrics.py | 4 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/schema.py | 5 | ||||
-rw-r--r-- | python/knot_resolver/client/commands/validate.py | 4 | ||||
-rw-r--r-- | python/knot_resolver/client/main.py | 2 | ||||
-rw-r--r-- | utils/meson.build | 1 | ||||
-rw-r--r-- | utils/shell-completion/client.bash | 38 | ||||
-rw-r--r-- | utils/shell-completion/meson.build | 12 |
21 files changed, 248 insertions, 190 deletions
@@ -1,12 +1,12 @@ Knot Resolver 6.0.10 (202y-mm-dd) -================================ +================================= Improvements ------------ - avoid multiple log lines when IPv6 isn't available (!1633) - manager: fix startup on Linux without libsystemd (!1608) - auto-reload TLS certificate files (!1626) - +- kresctl: bash command-line TAB completion (!1622) Knot Resolver 6.0.9 (2024-11-11) ================================ diff --git a/daemon/engine.c b/daemon/engine.c index 509915df..a0da529b 100644 --- a/daemon/engine.c +++ b/daemon/engine.c @@ -741,8 +741,6 @@ int engine_register(const char *name, const char *precedence, const char* ref) if (!module) { return kr_error(ENOMEM); } - module->data = the_engine; /*< some outside modules may still use this value */ - int ret = kr_module_load(module, name, LIBDIR "/kres_modules"); if (ret == 0) { /* We have a C module, loaded and init() was called. diff --git a/distro/pkg/deb/knot-resolver6.install b/distro/pkg/deb/knot-resolver6.install index 7b9d0c41..068f6c19 100644 --- a/distro/pkg/deb/knot-resolver6.install +++ b/distro/pkg/deb/knot-resolver6.install @@ -34,3 +34,4 @@ usr/lib/systemd/system/knot-resolver.service usr/lib/tmpfiles.d/knot-resolver.conf usr/sbin/kres-cache-gc usr/sbin/kresd +usr/share/bash-completion/completions/kresctl diff --git a/distro/pkg/rpm/knot-resolver.spec b/distro/pkg/rpm/knot-resolver.spec index 91c1a148..504ae7ed 100644 --- a/distro/pkg/rpm/knot-resolver.spec +++ b/distro/pkg/rpm/knot-resolver.spec @@ -296,6 +296,7 @@ getent passwd knot-resolver >/dev/null || useradd -r -g knot-resolver -d %{_sysc %{python3_sitearch}/knot_resolver* %{_mandir}/man8/kresd.8.gz %{_mandir}/man8/kresctl.8.gz +%{_datadir}/bash-completion/completions/kresctl %files devel %{_includedir}/libkres diff --git a/lib/module.c b/lib/module.c index 79219d3a..df9a1f5e 100644 --- a/lib/module.c +++ b/lib/module.c @@ -103,10 +103,8 @@ int kr_module_load(struct kr_module *module, const char *name, const char *path) return kr_error(EINVAL); } - /* Initialize, keep userdata */ - void *data = module->data; + /* Initialize */ memset(module, 0, sizeof(struct kr_module)); - module->data = data; module->name = strdup(name); if (module->name == NULL) { return kr_error(ENOMEM); @@ -123,6 +121,8 @@ int kr_module_load(struct kr_module *module, const char *name, const char *path) ret = module->init(module); } if (ret != 0) { + /* Avoid calling deinit() as init() wasn't called or failed. */ + module->deinit = NULL; kr_module_unload(module); } diff --git a/lib/module.h b/lib/module.h index 507b2df1..4dd5a490 100644 --- a/lib/module.h +++ b/lib/module.h @@ -86,7 +86,7 @@ struct kr_prop { /** * Load a C module instance into memory. And call its init(). * - * @param module module structure. Will be overwritten except for ->data on success. + * @param module module structure. Will be overwritten. * @param name module name * @param path module search path * @return 0 or an error diff --git a/modules/stats/stats.c b/modules/stats/stats.c index 596847d7..09a0cfdc 100644 --- a/modules/stats/stats.c +++ b/modules/stats/stats.c @@ -624,6 +624,9 @@ int stats_init(struct kr_module *module) /* Initialize ring buffer of recently visited upstreams */ array_init(data->upstreams.q); if (array_reserve(data->upstreams.q, UPSTREAMS_COUNT) != 0) { + trie_free(data->trie); + lru_free(data->queries.frequent); + free(data); return kr_error(ENOMEM); } data->upstreams.q.len = UPSTREAMS_COUNT; /* signify we use the entries */ diff --git a/python/knot_resolver/client/command.py b/python/knot_resolver/client/command.py index 76c0f1d0..3966f8ca 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 abc import ABC, abstractmethod from pathlib import Path -from typing import Dict, List, Optional, Tuple, Type, TypeVar +from typing import Dict, List, Optional, Set, Tuple, Type, TypeVar from urllib.parse import quote from knot_resolver.constants import API_SOCK_FILE, CONFIG_FILE @@ -14,9 +14,123 @@ T = TypeVar("T", bound=Type["Command"]) CompWords = Dict[str, Optional[str]] +COMP_DIRNAMES = "#dirnames#" +COMP_FILENAMES = "#filenames#" +COMP_NOSPACE = "#nospace#" + _registered_commands: List[Type["Command"]] = [] +def get_mutually_exclusive_args(parser: argparse.ArgumentParser) -> List[Set[str]]: + groups: List[Set[str]] = [] + + for group in parser._mutually_exclusive_groups: # noqa: SLF001 + group_args: Set[str] = set() + for action in group._group_actions: # noqa: SLF001 + if action.option_strings: + group_args.update(action.option_strings) + if group_args: + groups.append(group_args) + return groups + + +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 parser_actions: + if isinstance(action, argparse._SubParsersAction) and action.choices: # noqa: SLF001 + for choice, parser in action.choices.items(): + 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 comp_get_words(args: List[str], parser: argparse.ArgumentParser) -> CompWords: # noqa: PLR0912 + words: CompWords = comp_get_actions_words(parser._actions) # noqa: SLF001 + 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: + continue + + if i + 1 >= nargs: + continue + + # remove exclusive arguments from words + for exclusive_args in get_mutually_exclusive_args(parser): + if arg in exclusive_args: + for earg in exclusive_args: + if earg in words.keys(): + del words[earg] + # remove alternative arguments from words + for opt in action.option_strings: + if opt in words.keys(): + del words[opt] + + # 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 + if i + 2 >= nargs: + 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: Optional[argparse.ArgumentParser] = action.choices[arg] if arg in action.choices else None + + command = get_subparser_command(subparser) if subparser else None + if command and subparser: + return command.completion(args[i + 1 :], subparser) + if subparser: + return comp_get_words(args[i + 1 :], subparser) # noqa: SLF001 + return {} + + return words + + def register_command(cls: T) -> T: _registered_commands.append(cls) return cls diff --git a/python/knot_resolver/client/commands/cache.py b/python/knot_resolver/client/commands/cache.py index 60417eec..a1bebeed 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, 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 {} + return comp_get_words(args, parser) 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 05fdded8..5e3d3628 100644 --- a/python/knot_resolver/client/commands/completion.py +++ b/python/knot_resolver/client/commands/completion.py @@ -2,7 +2,13 @@ import argparse from enum import Enum 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, +) class Shells(Enum): @@ -15,29 +21,17 @@ class CompletionCommand(Command): def __init__(self, namespace: argparse.Namespace) -> None: super().__init__(namespace) self.shell: Shells = namespace.shell - self.space = namespace.space - self.comp_args: List[str] = namespace.comp_args - - if self.space: - self.comp_args.append("") + self.args: List[str] = namespace.args + if namespace.extra is not None: + self.args.append("--") @staticmethod def register_args_subparser( subparser: "argparse._SubParsersAction[argparse.ArgumentParser]", ) -> Tuple[argparse.ArgumentParser, "Type[Command]"]: - completion = subparser.add_parser("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, - ) - completion.add_argument( - "comp_args", - type=str, - help="arguments to complete", - nargs="*", + completion = subparser.add_parser( + "completion", + help="commands auto-completion", ) shells_dest = "shell" @@ -45,51 +39,27 @@ class CompletionCommand(Command): shells.add_argument("--bash", action="store_const", dest=shells_dest, const=Shells.BASH, default=Shells.BASH) shells.add_argument("--fish", action="store_const", dest=shells_dest, const=Shells.FISH) + completion.add_argument("--args", help="arguments to complete", nargs=argparse.REMAINDER, default=[]) + return completion, CompletionCommand @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - words: CompWords = {} - # for action in parser._actions: - # for opt in action.option_strings: - # words[opt] = action.help - # return words - return words + return comp_get_words(args, parser) - 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 + def run(self, args: CommandArgs) -> None: # noqa: PLR0912 + words: CompWords = {} - # 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}") + parser = args.parser + if parser: + words = comp_get_words(self.args, args.parser) - # # 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}") + # 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}") diff --git a/python/knot_resolver/client/commands/config.py b/python/knot_resolver/client/commands/config.py index 52df39c4..d13d24d9 100644 --- a/python/knot_resolver/client/commands/config.py +++ b/python/knot_resolver/client/commands/config.py @@ -3,7 +3,8 @@ import sys from enum import Enum from typing import List, Literal, Optional, Tuple, Type -from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command +from knot_resolver.client.command import COMP_NOSPACE, 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 @@ -22,56 +23,6 @@ def operation_to_method(operation: Operations) -> Literal["PUT", "GET", "DELETE" return "GET" -# def _properties_words(props: Dict[str, Any]) -> CompWords: -# words: CompWords = {} -# for name, prop in props.items(): -# words[name] = prop["description"] if "description" in prop else None -# return words - - -# def _path_comp_words(node: str, nodes: List[str], props: Dict[str, Any]) -> CompWords: -# i = nodes.index(node) -# ln = len(nodes[i:]) - -# # if node is last in path, return all possible words on thi level -# if ln == 1: -# return _properties_words(props) -# # if node is valid -# elif node in props: -# node_schema = props[node] - -# if "anyOf" in node_schema: -# for item in node_schema["anyOf"]: -# print(item) - -# elif "type" not in node_schema: -# pass - -# elif node_schema["type"] == "array": -# if ln > 2: -# # skip index for item in array -# return _path_comp_words(nodes[i + 2], nodes, node_schema["items"]["properties"]) -# if "enum" in node_schema["items"]: -# print(node_schema["items"]["enum"]) -# return {"0": "first array item", "-": "last array item"} -# elif node_schema["type"] == "object": -# if "additionalProperties" in node_schema: -# print(node_schema) -# return _path_comp_words(nodes[i + 1], nodes, node_schema["properties"]) -# return {} - -# # arrays/lists must be handled sparately -# if node_schema["type"] == "array": -# if ln > 2: -# # skip index for item in array -# return _path_comp_words(nodes[i + 2], nodes, node_schema["items"]["properties"]) -# return {"0": "first array item", "-": "last array item"} -# return _path_comp_words(nodes[i + 1], nodes, node_schema["properties"]) -# else: -# # if node is not last or valid, value error -# raise ValueError(f"unknown config path node: {node}") - - @register_command class ConfigCommand(Command): def __init__(self, namespace: argparse.Namespace) -> None: @@ -141,7 +92,7 @@ class ConfigCommand(Command): value_or_file = set_op.add_mutually_exclusive_group() value_or_file.add_argument( "file", - help="Optional, path to file with new configuraion.", + help="Optional, path to file with new configuration.", type=str, nargs="?", ) @@ -165,25 +116,50 @@ class ConfigCommand(Command): type=str, default="", ) - return config, ConfigCommand @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - # words = parser_words(parser._actions) # pylint: disable=W0212 - - # for arg in args: - # if arg in words: - # continue - # elif arg.startswith("-"): - # return words - # elif arg == args[-1]: - # config_path = arg[1:].split("/") if arg.startswith("/") else arg.split("/") - # schema_props: Dict[str, Any] = KresConfig.json_schema()["properties"] - # return _path_comp_words(config_path[0], config_path, schema_props) - # else: - # break - return {} + nargs = len(args) + + if nargs > 1 and args[-2] in ["-p", "--path"]: + words: CompWords = {} + words[COMP_NOSPACE] = None + + path = args[-1] + path_nodes = path.split("/") + + prefix = "" + properties = KresConfig.json_schema()["properties"] + is_list = False + for i, node in enumerate(path_nodes): + # first node is empty string + if i == 0: + continue + + if node in properties: + is_list = False + if "properties" in properties[node]: + properties = properties[node]["properties"] + prefix += f"/{node}" + continue + if "items" in properties[node]: + properties = properties[node]["items"]["properties"] + prefix += f"/{node}" + is_list = True + continue + del words[COMP_NOSPACE] + break + if is_list and node.isnumeric(): + prefix += f"/{node}" + continue + + for key in properties.keys(): + words[f"{prefix}/{key}"] = properties[key]["description"] + + return words + + return comp_get_words(args, parser) def run(self, args: CommandArgs) -> None: if not self.operation: diff --git a/python/knot_resolver/client/commands/convert.py b/python/knot_resolver/client/commands/convert.py index 412ed334..aab07519 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, 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 {} + return comp_get_words(args, parser) 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..14341e9d 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 @@ -19,7 +19,7 @@ class DebugCommand(Command): self.sudo: bool = namespace.sudo self.gdb: str = namespace.gdb self.print_only: bool = namespace.print_only - self.gdb_args: List[str] = namespace.extra + self.gdb_args: List[str] = namespace.extra if namespace.extra is not None else [] super().__init__(namespace) @staticmethod @@ -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) 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..94942091 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) @staticmethod def register_args_subparser( diff --git a/python/knot_resolver/client/commands/metrics.py b/python/knot_resolver/client/commands/metrics.py index 058cad8b..57ff9171 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, 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 {} + return comp_get_words(args, parser) 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 0c63f398..c5e4dfc4 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, 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,8 +35,7 @@ class SchemaCommand(Command): @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - return {} - # return parser_words(parser._actions) # pylint: disable=W0212 + return comp_get_words(args, parser) 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 f7477748..92848b58 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, 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 {} + return comp_get_words(args, parser) def run(self, args: CommandArgs) -> None: if self.input_file: diff --git a/python/knot_resolver/client/main.py b/python/knot_resolver/client/main.py index 461b7fc4..683bc95b 100644 --- a/python/knot_resolver/client/main.py +++ b/python/knot_resolver/client/main.py @@ -77,7 +77,7 @@ def main() -> None: argv_extra = sys.argv[(pa_index + 1) :] except ValueError: argv_to_parse = sys.argv[1:] - argv_extra = [] + argv_extra = None namespace = parser.parse_args(argv_to_parse) if hasattr(namespace, "extra"): diff --git a/utils/meson.build b/utils/meson.build index 8bab5f2d..e6885a33 100644 --- a/utils/meson.build +++ b/utils/meson.build @@ -4,3 +4,4 @@ build_utils = get_option('utils') != 'disabled' subdir('cache_gc') +subdir('shell-completion') diff --git a/utils/shell-completion/client.bash b/utils/shell-completion/client.bash index b3c19419..5cf66723 100644 --- a/utils/shell-completion/client.bash +++ b/utils/shell-completion/client.bash @@ -3,31 +3,27 @@ _kresctl_completion() { COMPREPLY=() - local cur prev opts + local args="" + local words="" + local cur="${COMP_WORDS[COMP_CWORD]}" + local opts=$(kresctl completion --bash --args "${COMP_WORDS[@]:1}") - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - # check if there is a word is empty - # that means there is a space after last non-empty word - if [[ -z "$cur" ]] - then - # no word to complete, return all posible options - opts=$(kresctl completion --bash --space "${COMP_WORDS}") - else - opts=$(kresctl completion --bash "${COMP_WORDS}") - fi - - # if there is no completion from kresctl - # auto-complete just directories and files - if [[ -z "$opts" ]] - then - COMPREPLY=($(compgen -d -f "${cur}")) + # filter special opts + for opt in $opts + do + if [[ "$opt" == "#dirnames#" ]]; then + args="$args${args:+ }-d" + elif [[ "$opt" == "#filenames#" ]]; then + args="$args${args:+ }-f" + elif [[ "$opt" == "#nospace#" ]]; then + compopt -o nospace else - COMPREPLY=( $(compgen -W "${opts}" ${cur}) ) + words="$words${words:+ }$opt" fi + done + COMPREPLY=($(compgen $args -W "${words}" -- "${cur}")) return 0 } -complete -o filenames -o dirnames -F _kresctl_completion kresctl +complete -o nosort -F _kresctl_completion kresctl diff --git a/utils/shell-completion/meson.build b/utils/shell-completion/meson.build index 6c35ffe3..7f4d3601 100644 --- a/utils/shell-completion/meson.build +++ b/utils/shell-completion/meson.build @@ -1,4 +1,4 @@ -# CLI comletion for bash-shell +# CLI completion for bash-shell install_data( sources: 'client.bash', rename: 'kresctl', @@ -6,8 +6,8 @@ install_data( ) # CLI completion for fish-shell -install_data( - sources: 'client.fish', - rename: 'kresctl.fish', - install_dir: completion_dir / 'fish' / 'completions' - ) +# install_data( +# sources: 'client.fish', +# rename: 'kresctl.fish', +# install_dir: completion_dir / 'fish' / 'completions' +# ) |