Fix Bash completion for sub-actions like "borgmatic config bootstrap" (#697 follow-on work).
This commit is contained in:
parent
1d7c7eaaa7
commit
bbc7f0596c
15 changed files with 170 additions and 82 deletions
2
NEWS
2
NEWS
|
@ -1,7 +1,7 @@
|
||||||
1.7.15.dev0
|
1.7.15.dev0
|
||||||
* #697: Extract borgmatic configuration from backup via "bootstrap" action—even when borgmatic
|
* #697: Extract borgmatic configuration from backup via "bootstrap" action—even when borgmatic
|
||||||
has no configuration yet!
|
has no configuration yet!
|
||||||
* #669: Add sample systemd user serivce for running borgmatic as a non-root user.
|
* #669: Add sample systemd user service for running borgmatic as a non-root user.
|
||||||
|
|
||||||
1.7.14
|
1.7.14
|
||||||
* #484: Add a new verbosity level (-2) to disable output entirely (for console, syslog, log file,
|
* #484: Add a new verbosity level (-2) to disable output entirely (for console, syslog, log file,
|
||||||
|
|
|
@ -31,7 +31,8 @@ import borgmatic.actions.restore
|
||||||
import borgmatic.actions.rinfo
|
import borgmatic.actions.rinfo
|
||||||
import borgmatic.actions.rlist
|
import borgmatic.actions.rlist
|
||||||
import borgmatic.actions.transfer
|
import borgmatic.actions.transfer
|
||||||
import borgmatic.commands.completion
|
import borgmatic.commands.completion.bash
|
||||||
|
import borgmatic.commands.completion.fish
|
||||||
from borgmatic.borg import umount as borg_umount
|
from borgmatic.borg import umount as borg_umount
|
||||||
from borgmatic.borg import version as borg_version
|
from borgmatic.borg import version as borg_version
|
||||||
from borgmatic.commands.arguments import parse_arguments
|
from borgmatic.commands.arguments import parse_arguments
|
||||||
|
@ -751,10 +752,10 @@ def main(): # pragma: no cover
|
||||||
print(importlib_metadata.version('borgmatic'))
|
print(importlib_metadata.version('borgmatic'))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if global_arguments.bash_completion:
|
if global_arguments.bash_completion:
|
||||||
print(borgmatic.commands.completion.bash_completion())
|
print(borgmatic.commands.completion.bash.bash_completion())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if global_arguments.fish_completion:
|
if global_arguments.fish_completion:
|
||||||
print(borgmatic.commands.completion.fish_completion())
|
print(borgmatic.commands.completion.fish.fish_completion())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
|
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
|
||||||
|
|
0
borgmatic/commands/completion/__init__.py
Normal file
0
borgmatic/commands/completion/__init__.py
Normal file
44
borgmatic/commands/completion/actions.py
Normal file
44
borgmatic/commands/completion/actions.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_message(language: str, upgrade_command: str, completion_file: str):
|
||||||
|
return f'''
|
||||||
|
Your {language} 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:
|
||||||
|
|
||||||
|
{upgrade_command}
|
||||||
|
source {completion_file}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def available_actions(subparsers, current_action=None):
|
||||||
|
'''
|
||||||
|
Given subparsers as an argparse._SubParsersAction instance and a current action name (if
|
||||||
|
any), return the actions names that can follow the current action on a command-line.
|
||||||
|
|
||||||
|
This takes into account which sub-actions that the current action supports. For instance, if
|
||||||
|
"bootstrap" is a sub-action for "config", then "bootstrap" should be able to follow a current
|
||||||
|
action of "config" but not "list".
|
||||||
|
'''
|
||||||
|
# Make a map from action name to the names of contained sub-actions.
|
||||||
|
actions_to_subactions = {
|
||||||
|
action: tuple(
|
||||||
|
subaction_name
|
||||||
|
for subaction in subparser._actions
|
||||||
|
if isinstance(subaction, argparse._SubParsersAction)
|
||||||
|
for subaction_name in subaction.choices.keys()
|
||||||
|
)
|
||||||
|
for action, subparser in subparsers.choices.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
current_subactions = actions_to_subactions.get(current_action)
|
||||||
|
|
||||||
|
if current_subactions:
|
||||||
|
return current_subactions
|
||||||
|
|
||||||
|
all_subactions = set(
|
||||||
|
subaction for subactions in actions_to_subactions.values() for subaction in subactions
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(action for action in subparsers.choices.keys() if action not in all_subactions)
|
62
borgmatic/commands/completion/bash.py
Normal file
62
borgmatic/commands/completion/bash.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import borgmatic.commands.arguments
|
||||||
|
import borgmatic.commands.completion.actions
|
||||||
|
|
||||||
|
|
||||||
|
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 = borgmatic.commands.arguments.make_parsers()
|
||||||
|
global_flags = parser_flags(top_level_parser)
|
||||||
|
|
||||||
|
# Avert your eyes.
|
||||||
|
return '\n'.join(
|
||||||
|
(
|
||||||
|
'check_version() {',
|
||||||
|
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
|
||||||
|
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
|
||||||
|
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
|
||||||
|
f''' then cat << EOF\n{borgmatic.commands.completion.actions.upgrade_message(
|
||||||
|
'bash',
|
||||||
|
'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
|
||||||
|
'$BASH_SOURCE',
|
||||||
|
)}\nEOF''',
|
||||||
|
' 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),
|
||||||
|
' '.join(
|
||||||
|
borgmatic.commands.completion.actions.available_actions(subparsers, action)
|
||||||
|
),
|
||||||
|
global_flags,
|
||||||
|
)
|
||||||
|
for action, subparser in reversed(subparsers.choices.items())
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
' COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))' # noqa: FS003
|
||||||
|
% (
|
||||||
|
' '.join(borgmatic.commands.completion.actions.available_actions(subparsers)),
|
||||||
|
global_flags,
|
||||||
|
),
|
||||||
|
' (check_version &)',
|
||||||
|
'}',
|
||||||
|
'\ncomplete -o bashdefault -o default -F complete_borgmatic borgmatic',
|
||||||
|
)
|
||||||
|
)
|
|
@ -2,72 +2,8 @@ import shlex
|
||||||
from argparse import Action
|
from argparse import Action
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from borgmatic.commands import arguments
|
import borgmatic.commands.arguments
|
||||||
|
import borgmatic.commands.completion.actions
|
||||||
|
|
||||||
def upgrade_message(language: str, upgrade_command: str, completion_file: str):
|
|
||||||
return f'''
|
|
||||||
Your {language} 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:
|
|
||||||
|
|
||||||
{upgrade_command}
|
|
||||||
source {completion_file}
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
# Avert your eyes.
|
|
||||||
return '\n'.join(
|
|
||||||
(
|
|
||||||
'check_version() {',
|
|
||||||
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
|
|
||||||
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
|
|
||||||
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
|
|
||||||
f''' then cat << EOF\n{upgrade_message(
|
|
||||||
'bash',
|
|
||||||
'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
|
|
||||||
'$BASH_SOURCE',
|
|
||||||
)}\nEOF''',
|
|
||||||
' 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]}"))' # noqa: FS003
|
|
||||||
% (actions, global_flags),
|
|
||||||
' (check_version &)',
|
|
||||||
'}',
|
|
||||||
'\ncomplete -o bashdefault -o default -F complete_borgmatic borgmatic',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# fish section
|
|
||||||
|
|
||||||
|
|
||||||
def has_file_options(action: Action):
|
def has_file_options(action: Action):
|
||||||
|
@ -155,7 +91,7 @@ def fish_completion():
|
||||||
Return a fish completion script for the borgmatic command. Produce this by introspecting
|
Return a fish completion script for the borgmatic command. Produce this by introspecting
|
||||||
borgmatic's command-line argument parsers.
|
borgmatic's command-line argument parsers.
|
||||||
'''
|
'''
|
||||||
top_level_parser, subparsers = arguments.make_parsers()
|
top_level_parser, subparsers = borgmatic.commands.arguments.make_parsers()
|
||||||
|
|
||||||
all_subparsers = ' '.join(action for action in subparsers.choices.keys())
|
all_subparsers = ' '.join(action for action in subparsers.choices.keys())
|
||||||
|
|
||||||
|
@ -182,7 +118,7 @@ def fish_completion():
|
||||||
set this_script (cat $this_filename 2> /dev/null)
|
set this_script (cat $this_filename 2> /dev/null)
|
||||||
set installed_script (borgmatic --fish-completion 2> /dev/null)
|
set installed_script (borgmatic --fish-completion 2> /dev/null)
|
||||||
if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ]
|
if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ]
|
||||||
echo "{upgrade_message(
|
echo "{borgmatic.commands.completion.actions.upgrade_message(
|
||||||
'fish',
|
'fish',
|
||||||
'borgmatic --fish-completion | sudo tee $this_filename',
|
'borgmatic --fish-completion | sudo tee $this_filename',
|
||||||
'$this_filename',
|
'$this_filename',
|
0
tests/integration/commands/completion/__init__.py
Normal file
0
tests/integration/commands/completion/__init__.py
Normal file
20
tests/integration/commands/completion/test_actions.py
Normal file
20
tests/integration/commands/completion/test_actions.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import borgmatic.commands.arguments
|
||||||
|
from borgmatic.commands.completion import actions as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_actions_uses_only_subactions_for_action_with_subactions():
|
||||||
|
unused_top_level_parser, subparsers = borgmatic.commands.arguments.make_parsers()
|
||||||
|
|
||||||
|
actions = module.available_actions(subparsers, 'config')
|
||||||
|
|
||||||
|
assert 'bootstrap' in actions
|
||||||
|
assert 'list' not in actions
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_actions_omits_subactions_for_action_without_subactions():
|
||||||
|
unused_top_level_parser, subparsers = borgmatic.commands.arguments.make_parsers()
|
||||||
|
|
||||||
|
actions = module.available_actions(subparsers, 'list')
|
||||||
|
|
||||||
|
assert 'bootstrap' not in actions
|
||||||
|
assert 'config' in actions
|
5
tests/integration/commands/completion/test_bash.py
Normal file
5
tests/integration/commands/completion/test_bash.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from borgmatic.commands.completion import bash as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_bash_completion_does_not_raise():
|
||||||
|
assert module.bash_completion()
|
5
tests/integration/commands/completion/test_fish.py
Normal file
5
tests/integration/commands/completion/test_fish.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from borgmatic.commands.completion import fish as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_fish_completion_does_not_raise():
|
||||||
|
assert module.fish_completion()
|
|
@ -1,9 +0,0 @@
|
||||||
from borgmatic.commands import completion as module
|
|
||||||
|
|
||||||
|
|
||||||
def test_bash_completion_does_not_raise():
|
|
||||||
assert module.bash_completion()
|
|
||||||
|
|
||||||
|
|
||||||
def test_fish_completion_does_not_raise():
|
|
||||||
assert module.fish_completion()
|
|
0
tests/unit/commands/completion/__init__.py
Normal file
0
tests/unit/commands/completion/__init__.py
Normal file
7
tests/unit/commands/completion/test_actions.py
Normal file
7
tests/unit/commands/completion/test_actions.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from borgmatic.commands.completion import actions as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_upgrade_message_does_not_raise():
|
||||||
|
module.upgrade_message(
|
||||||
|
language='English', upgrade_command='read a lot', completion_file='your brain'
|
||||||
|
)
|
17
tests/unit/commands/completion/test_bash.py
Normal file
17
tests/unit/commands/completion/test_bash.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.commands.completion import bash as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_parser_flags_flattens_and_joins_flags():
|
||||||
|
assert (
|
||||||
|
module.parser_flags(
|
||||||
|
flexmock(
|
||||||
|
_actions=[
|
||||||
|
flexmock(option_strings=['--foo', '--bar']),
|
||||||
|
flexmock(option_strings=['--baz']),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
== '--foo --bar --baz'
|
||||||
|
)
|
|
@ -5,7 +5,7 @@ from typing import Tuple
|
||||||
import pytest
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.commands import completion as module
|
from borgmatic.commands.completion import fish as module
|
||||||
|
|
||||||
OptionType = namedtuple('OptionType', ['file', 'choice', 'unknown_required'])
|
OptionType = namedtuple('OptionType', ['file', 'choice', 'unknown_required'])
|
||||||
TestCase = Tuple[Action, OptionType]
|
TestCase = Tuple[Action, OptionType]
|
Loading…
Reference in a new issue