]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
kresctl: tab-completion: stop appending space after one config layer is completed docs-develop-kres-3ujg2y/deployments/5887 kresctl-comp-config-levels
authorFrantisek Tobias <frantisek.tobias@nic.cz>
Tue, 10 Dec 2024 07:23:44 +0000 (08:23 +0100)
committerFrantisek Tobias <frantisek.tobias@nic.cz>
Fri, 13 Dec 2024 15:14:39 +0000 (16:14 +0100)
python/knot_resolver/client/command.py
python/knot_resolver/client/commands/config.py
utils/shell-completion/client.bash

index 33f2c2ad96b06bbd430941ff34bda013f5dd8fc8..a34f31dc744eef82b47d5f5ae7226aa7ccfa6e99 100644 (file)
@@ -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 Any, Dict, List, Optional, Set, Tuple, Type, TypeVar
 from urllib.parse import quote
 
 from knot_resolver.constants import API_SOCK_FILE, CONFIG_FILE
@@ -17,15 +17,43 @@ CompWords = Dict[str, Optional[str]]
 _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
 
 
@@ -136,34 +164,54 @@ class Command(ABC):
         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
index cf8f2019e4684866df21a6f8028f8a28bb8050a2..eee42741fb8ae2460ca56afc1a32cefb626e3ac3 100644 (file)
@@ -1,7 +1,7 @@
 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
@@ -23,22 +23,15 @@ def operation_to_method(operation: Operations) -> Literal["PUT", "GET", "DELETE"
     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:
@@ -50,8 +43,6 @@ def generate_paths(data: Dict[str, Any], prefix: str = "/") -> CompWords:
                     paths.update(generate_paths(item, prefix))
             else:
                 paths.update(generate_paths(data["items"], prefix))
-        else:
-            paths[prefix] = None
 
     return paths
 
@@ -78,14 +69,22 @@ class ConfigCommand(Command):
         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.",
@@ -113,8 +112,15 @@ class ConfigCommand(Command):
         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",
@@ -141,8 +147,15 @@ class ConfigCommand(Command):
             "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",
@@ -153,15 +166,17 @@ class ConfigCommand(Command):
         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:
@@ -169,18 +184,20 @@ class ConfigCommand(Command):
                     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:
index de61c429514a1697cb19d65bcee9794be7b0605f..56fa6d9d50db4f41ba69adbebc7ef586fdb4f816 100644 (file)
@@ -1,39 +1,28 @@
-#/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
@@ -48,4 +37,4 @@ _kresctl_completion()
     return 0
 }
 
-complete -o filenames -o dirnames -o nosort -F _kresctl_completion kresctl
+complete -o filenames -o dirnames -F _kresctl_completion kresctl