summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukáš Ondráček <lukas.ondracek@nic.cz>2025-01-02 14:38:53 +0100
committerLukáš Ondráček <lukas.ondracek@nic.cz>2025-01-02 14:38:53 +0100
commita01dbeba1852a6cd71e81fb99f65e0810ec0046f (patch)
tree2347fb0d57801f13279af9f4c1435def5af50990
parentdaemon/defer: fix configuration reload (diff)
parentMerge branch 'kresctl-tab-completion' into 'master' (diff)
downloadknot-resolver-a01dbeba1852a6cd71e81fb99f65e0810ec0046f.tar.xz
knot-resolver-a01dbeba1852a6cd71e81fb99f65e0810ec0046f.zip
Merge branch 'master' into defer-wip
-rw-r--r--NEWS4
-rw-r--r--daemon/engine.c2
-rw-r--r--distro/pkg/deb/knot-resolver6.install1
-rw-r--r--distro/pkg/rpm/knot-resolver.spec1
-rw-r--r--lib/module.c6
-rw-r--r--lib/module.h2
-rw-r--r--modules/stats/stats.c3
-rw-r--r--python/knot_resolver/client/command.py118
-rw-r--r--python/knot_resolver/client/commands/cache.py4
-rw-r--r--python/knot_resolver/client/commands/completion.py90
-rw-r--r--python/knot_resolver/client/commands/config.py110
-rw-r--r--python/knot_resolver/client/commands/convert.py6
-rw-r--r--python/knot_resolver/client/commands/debug.py21
-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.py5
-rw-r--r--python/knot_resolver/client/commands/validate.py4
-rw-r--r--python/knot_resolver/client/main.py2
-rw-r--r--utils/meson.build1
-rw-r--r--utils/shell-completion/client.bash38
-rw-r--r--utils/shell-completion/meson.build12
21 files changed, 248 insertions, 190 deletions
diff --git a/NEWS b/NEWS
index 05061221..e1562b6f 100644
--- a/NEWS
+++ b/NEWS
@@ -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'
+# )