Browse Source

Add Bash completion script so you can tab-complete the borgmatic command-line.

Dan Helfman 3 years ago
parent
commit
77b84f8a48

+ 3 - 0
NEWS

@@ -1,6 +1,9 @@
 1.6.2.dev0
  * #536: Fix generate-borgmatic-config with "--source" flag to support more complex schema changes
    like the new Healthchecks configuration options.
+ * Add Bash completion script so you can tab-complete the borgmatic command-line. See the
+   documentation for more information:
+   https://torsion.org/borgmatic/docs/how-to/set-up-backups/#shell-completion
 
 1.6.1
  * #294: Add Healthchecks monitoring hook "ping_body_limit" option to configure how many bytes of

+ 18 - 3
borgmatic/commands/arguments.py

@@ -109,10 +109,9 @@ class Extend_action(Action):
             setattr(namespace, self.dest, list(values))
 
 
-def parse_arguments(*unparsed_arguments):
+def make_parsers():
     '''
-    Given command-line arguments with which this script was invoked, parse the arguments and return
-    them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
+    Build a top-level parser and its subparsers and return them as a tuple.
     '''
     config_paths = collect.get_default_config_paths(expand_home=True)
     unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
@@ -189,6 +188,12 @@ def parse_arguments(*unparsed_arguments):
         action='extend',
         help='One or more configuration file options to override with specified values',
     )
+    global_group.add_argument(
+        '--bash-completion',
+        default=False,
+        action='store_true',
+        help='Show bash completion script and exit',
+    )
     global_group.add_argument(
         '--version',
         dest='version',
@@ -647,6 +652,16 @@ def parse_arguments(*unparsed_arguments):
     )
     borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
 
+    return top_level_parser, subparsers
+
+
+def parse_arguments(*unparsed_arguments):
+    '''
+    Given command-line arguments with which this script was invoked, parse the arguments and return
+    them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
+    '''
+    top_level_parser, subparsers = make_parsers()
+
     arguments, remaining_arguments = parse_subparser_arguments(
         unparsed_arguments, subparsers.choices
     )

+ 4 - 0
borgmatic/commands/borgmatic.py

@@ -11,6 +11,7 @@ from subprocess import CalledProcessError
 import colorama
 import pkg_resources
 
+import borgmatic.commands.completion
 from borgmatic.borg import borg as borg_borg
 from borgmatic.borg import check as borg_check
 from borgmatic.borg import compact as borg_compact
@@ -884,6 +885,9 @@ def main():  # pragma: no cover
     if global_arguments.version:
         print(pkg_resources.require('borgmatic')[0].version)
         sys.exit(0)
+    if global_arguments.bash_completion:
+        print(borgmatic.commands.completion.bash_completion())
+        sys.exit(0)
 
     config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
     configs, parse_logs = load_configurations(config_filenames, global_arguments.overrides)

+ 60 - 0
borgmatic/commands/completion.py

@@ -0,0 +1,60 @@
+import pkg_resources
+
+from borgmatic.commands import arguments
+
+UPGRADE_MESSAGE = '''
+Your bash completions script is from a different version of borgmatic than is
+currently installed. Please upgrade your script so your completions match the
+command-line flags in your installed borgmatic! Try this to upgrade:
+
+    sudo sh -c "borgmatic --bash-completions > $BASH_SOURCE"
+    source $BASH_SOURCE
+'''
+
+
+def parser_flags(parser):
+    '''
+    Given an argparse.ArgumentParser instance, return its argument flags in a space-separated
+    string.
+    '''
+    return ' '.join(option for action in parser._actions for option in action.option_strings)
+
+
+def bash_completion():
+    '''
+    Return a bash completion script for the borgmatic command. Produce this by introspecting
+    borgmatic's command-line argument parsers.
+    '''
+    top_level_parser, subparsers = arguments.make_parsers()
+    global_flags = parser_flags(top_level_parser)
+    actions = ' '.join(subparsers.choices.keys())
+    borgmatic_version = pkg_resources.require('borgmatic')[0].version
+
+    # Avert your eyes.
+    return '\n'.join(
+        (
+            'check_version() {',
+            '    local installed_version="$(borgmatic --version 2> /dev/null)"',
+            '    if [ "$installed_version" != "%s" ] && [ "$installed_version" != "" ];'
+            % borgmatic_version,
+            '        then cat << EOF\n%s\nEOF' % UPGRADE_MESSAGE,
+            '    fi',
+            '}',
+            'complete_borgmatic() {',
+        )
+        + tuple(
+            '''    if [[ " ${COMP_WORDS[*]} " =~ " %s " ]]; then
+        COMPREPLY=($(compgen -W "%s %s %s" -- "${COMP_WORDS[COMP_CWORD]}"))
+        return 0
+    fi'''
+            % (action, parser_flags(subparser), actions, global_flags)
+            for action, subparser in subparsers.choices.items()
+        )
+        + (
+            '    COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))'
+            % (actions, global_flags),
+            '    (check_version &)',
+            '}',
+            '\ncomplete -F complete_borgmatic borgmatic',
+        )
+    )

+ 28 - 2
docs/how-to/set-up-backups.md

@@ -111,6 +111,7 @@ Additionally, [rsync.net](https://www.rsync.net/products/borg.html) and
 [Hetzner](https://www.hetzner.com/storage/storage-box) have compatible storage
 offerings, but do not currently fund borgmatic development or hosting.
 
+
 ## Configuration
 
 After you install borgmatic, generate a sample configuration file:
@@ -302,9 +303,34 @@ interested in an [unofficial work-around for Full Disk
 Access](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/293).
 
 
-## Colored output
+## Niceties
+
+
+### Shell completion
+
+borgmatic includes a shell completion script (currently only for Bash) to
+support tab-completing borgmatic command-line actions and flags. Depending on
+how you installed borgmatic, this may be enabled by default. But if it's not,
+you can install the shell completion script globally:
+
+```bash
+sudo su -c "borgmatic --bash-completion > /usr/share/bash-completion/completions/borgmatic"
+```
+
+Alternatively, if you'd like to install the script for just the current user:
+
+```bash
+mkdir --parents ~/.local/share/bash-completion/completions
+borgmatic --bash-completion > ~/.local/share/bash-completion/completions/borgmatic
+```
+
+In either case, you may also need to install the `bash-completion` Linux
+package and restart your shell (`exit` and open a new shell).
+
+
+### Colored output
 
-Borgmatic produces colored terminal output by default. It is disabled when a
+borgmatic produces colored terminal output by default. It is disabled when a
 non-interactive terminal is detected (like a cron job), or when you use the
 `--json` flag. Otherwise, you can disable it by passing the `--no-color` flag,
 setting the environment variable `PY_COLORS=False`, or setting the `color`

+ 5 - 0
tests/end-to-end/test_completion.py

@@ -0,0 +1,5 @@
+import subprocess
+
+
+def test_bash_completion_runs_without_error():
+    subprocess.check_call('eval "$(borgmatic --bash-completion)"', shell=True)

+ 5 - 0
tests/integration/commands/test_completion.py

@@ -0,0 +1,5 @@
+from borgmatic.commands import completion as module
+
+
+def test_bash_completion_does_not_raise():
+    assert module.bash_completion()