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 Any, Dict, List, Optional, Set, Tuple, Type, TypeVar
from urllib.parse import quote
from knot_resolver.constants import API_SOCK_FILE, CONFIG_FILE
_registered_commands: List[Type["Command"]] = []
-def get_subparsers_words(subparser_actions: List[argparse.Action]) -> CompWords:
+def get_mutually_exclusive_commands(parser: argparse.ArgumentParser) -> List[Set[str]]:
+ command_names: List[Set[str]] = []
+ for group in parser._mutually_exclusive_groups: # pylint: disable=protected-access
+ command_names.append(set())
+ for action in group._group_actions: # pylint: disable=protected-access
+ if action.option_strings:
+ command_names[-1].update(action.option_strings)
+ return command_names
+
+
+def is_unique_and_new(arg: str, args: Set[str], exclusive: List[Set[str]], last: str) -> bool:
+ if arg not in args:
+ for excl in exclusive:
+ if arg in excl:
+ for cmd in excl:
+ if cmd in args:
+ return False
+ return True
+
+ return arg == last
+
+
+def get_subparsers_words(
+ subparser_actions: List[argparse.Action], args: Set[str], exclusive: List[Set[str]], last: str
+) -> CompWords:
+
words: CompWords = {}
for action in subparser_actions:
if isinstance(action, argparse._SubParsersAction) and action.choices: # pylint: disable=protected-access
for choice, parser in action.choices.items():
- words[choice] = parser.description
+ if is_unique_and_new(choice, args, exclusive, last):
+ words[choice] = parser.description
else:
for opt in action.option_strings:
- words[opt] = action.help
+ if is_unique_and_new(opt, args, exclusive, last):
+ words[opt] = action.help
+
return words
raise NotImplementedError()
@staticmethod
- def completion(parser: argparse.ArgumentParser, args: Optional[List[str]] = None, curr_index: int = 0) -> CompWords:
+ def completion(
+ parser: argparse.ArgumentParser,
+ args: Optional[List[str]] = None,
+ curr_index: int = 0,
+ argset: Optional[Set[str]] = None,
+ ) -> CompWords:
+
if args is None or len(args) == 0:
return {}
- words: CompWords = get_subparsers_words(parser._actions) # pylint: disable=protected-access
+ if argset is None:
+ argset = set(args)
- subparsers = parser._subparsers # pylint: disable=protected-access
+ if "-h" in argset or "--help" in argset:
+ return {args[-1]: None} if args[-1] in ["-h", "--help"] else {}
+ exclusive: List[Set[str]] = get_mutually_exclusive_commands(parser)
+
+ words = get_subparsers_words(parser._actions, argset, exclusive, args[-1]) # pylint: disable=protected-access
+
+ subparsers = parser._subparsers # pylint: disable=protected-access
if subparsers:
while curr_index < len(args):
uarg = args[curr_index]
- subpar = get_subparser_by_name(uarg, subparsers._actions) # pylint: disable=W0212
-
curr_index += 1
+
+ subpar = get_subparser_by_name(uarg, subparsers._actions) # pylint: disable=protected-access
if subpar:
cmd = get_subparser_command(subpar)
if cmd is None:
- return get_subparsers_words(subpar._actions) # pylint: disable=protected-access
+ exclusive = get_mutually_exclusive_commands(subpar)
+
+ if (curr_index >= len(args) or args[curr_index] == "") and uarg in words:
+ continue
- if len(args) > curr_index:
- return cmd.completion(subpar, args, curr_index)
+ words = get_subparsers_words(
+ subpar._actions, argset, exclusive, args[-1] # pylint: disable=protected-access
+ )
- return words
+ elif len(args) > curr_index:
+ words = cmd.completion(subpar, args, curr_index, argset)
- elif uarg in ["-s", "--socket", "-c", "--config"]:
- # following word shall not be a kresctl command, switch to path completion
+ break
+
+ if uarg in ["-s", "--socket", "-c", "--config"]:
if uarg in (args[-1], args[-2]):
words = {}
+
curr_index += 1
return words
import argparse
import sys
from enum import Enum
-from typing import Any, Dict, List, Literal, Optional, Tuple, Type
+from typing import Any, Dict, List, Literal, Optional, Set, Tuple, Type
from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command
from knot_resolver.datamodel import KresConfig
return "GET"
-def _properties_words(props: Dict[str, Any], prefix: str) -> CompWords:
- words: CompWords = {}
- for name in props:
- words[prefix + "/" + name] = props[name]["description"]
- return words
-
-
def generate_paths(data: Dict[str, Any], prefix: str = "/") -> CompWords:
paths = {}
if isinstance(data, dict):
if "properties" in data.keys():
for key in data["properties"]:
- current_path = f"{prefix}{key}/"
+ current_path = f"{prefix}{key}"
- new_paths = generate_paths(data["properties"][key], current_path)
+ new_paths = generate_paths(data["properties"][key], current_path + "/")
if new_paths != {}:
paths.update(new_paths)
else:
paths.update(generate_paths(item, prefix))
else:
paths.update(generate_paths(data["items"], prefix))
- else:
- paths[prefix] = None
return paths
get = config_subparsers.add_parser("get", help="Get current configuration from the resolver.")
get.set_defaults(operation=Operations.GET, format=DataFormat.YAML)
- get.add_argument(
+ get_path = get.add_mutually_exclusive_group()
+ get_path.add_argument(
"-p",
+ help=path_help,
+ action="store",
+ type=str,
+ default="",
+ )
+ get_path.add_argument(
"--path",
help=path_help,
action="store",
type=str,
default="",
)
+
get.add_argument(
"file",
help="Optional, path to the file where to save exported configuration data. If not specified, data will be printed.",
set = config_subparsers.add_parser("set", help="Set new configuration for the resolver.")
set.set_defaults(operation=Operations.SET)
- set.add_argument(
+ set_path = set.add_mutually_exclusive_group()
+ set_path.add_argument(
"-p",
+ help=path_help,
+ action="store",
+ type=str,
+ default="",
+ )
+ set_path.add_argument(
"--path",
help=path_help,
action="store",
"delete", help="Delete given configuration property or list item at the given index."
)
delete.set_defaults(operation=Operations.DELETE)
- delete.add_argument(
+ delete_path = delete.add_mutually_exclusive_group()
+ delete_path.add_argument(
"-p",
+ help=path_help,
+ action="store",
+ type=str,
+ default="",
+ )
+ delete_path.add_argument(
"--path",
help=path_help,
action="store",
return config, ConfigCommand
@staticmethod
- def completion(parser: argparse.ArgumentParser, args: Optional[List[str]] = None, curr_index: int = 0) -> CompWords:
- if args is not None and (len(args) - curr_index) > 1 and args[-2] in ["-p", "--path"]:
- # if len(args[-1]) < 2:
- # new_props = {}
- # for prop in props:
- # new_props['/' + prop] = props[prop]
- #
- # return new_props
+ def completion(
+ parser: argparse.ArgumentParser,
+ args: Optional[List[str]] = None,
+ curr_index: int = 0,
+ argset: Optional[Set[str]] = None,
+ ) -> CompWords:
+
+ if args is None or len(args) == 0:
+ return {}
+ if args is not None and (len(args) - curr_index) > 1 and args[-2] in {"-p", "--path"}:
paths = generate_paths(KresConfig.json_schema())
result = {}
for path in paths:
a_count = args[-1].count("/") + 1
new_path = ""
for c in path:
+ new_path += c
if c == "/":
a_count -= 1
if a_count == 0:
break
- new_path += c
-
- result[new_path + "/"] = paths[path]
+ result[new_path] = paths[path]
return result
- return Command.completion(parser, args, curr_index)
+ if argset is None:
+ argset = set(args)
+
+ return Command.completion(parser, args, curr_index, argset)
def run(self, args: CommandArgs) -> None:
if not self.operation:
-#/usr/bin/env bash
-
-_kresctl_filter_double_dash()
-{
- local words=("$@")
- local new_words=()
- local count=0
-
- for WORD in "${words[@]}"
- do
- if [[ "$WORD" != "--" ]]
- then
- new_words[count]="$WORD"
- ((count++))
- fi
- done
-
- printf "%s\n" "${new_words[@]}"
-}
+#!/usr/bin/env bash
_kresctl_completion()
{
COMPREPLY=()
- local cur opts cmp_words
+ local cur prev opts words_up_to_cursor
cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
local line="${COMP_LINE:0:$COMP_POINT}"
local words_up_to_cursor=($line)
- cmp_words=($(_kresctl_filter_double_dash "${words_up_to_cursor[@]}"))
-
if [[ -z "$cur" && "$COMP_POINT" -gt 0 && "${line: -1}" == " " ]]
then
- opts=$(kresctl completion --bash --space --args "${cmp_words[@]}")
+ opts=$(kresctl completion --bash --space --args "${words_up_to_cursor[@]}")
else
- opts=$(kresctl completion --bash --args "${cmp_words[@]}")
+ opts=$(kresctl completion --bash --args "${words_up_to_cursor[@]}")
+ fi
+
+ # if we're completing a config path do not append a space
+ # (unless we have reached the bottom)
+ if [[ "$prev" == "-p" || "$prev" == "--path" ]] \
+ && [[ $(echo "$opts" | wc -w) -gt 1 || "${opts: -1}" == '/' ]]
+ then
+ compopt -o nospace
fi
# if there is no completion from kresctl
return 0
}
-complete -o filenames -o dirnames -o nosort -F _kresctl_completion kresctl
+complete -o filenames -o dirnames -F _kresctl_completion kresctl