diff --git a/borgmatic/actions/config/bootstrap.py b/borgmatic/actions/config/bootstrap.py index f3b64594..5b323451 100644 --- a/borgmatic/actions/config/bootstrap.py +++ b/borgmatic/actions/config/bootstrap.py @@ -1,18 +1,20 @@ +import json import logging import os -import json import borgmatic.borg.extract import borgmatic.borg.rlist import borgmatic.config.validate import borgmatic.hooks.command - from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY logger = logging.getLogger(__name__) + def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version): - borgmatic_source_directory = bootstrap_arguments.borgmatic_source_directory or DEFAULT_BORGMATIC_SOURCE_DIRECTORY + borgmatic_source_directory = ( + bootstrap_arguments.borgmatic_source_directory or DEFAULT_BORGMATIC_SOURCE_DIRECTORY + ) borgmatic_manifest_path = os.path.expanduser( os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json') ) @@ -24,7 +26,7 @@ def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version): bootstrap_arguments.archive, {}, local_borg_version, - global_arguments + global_arguments, ), [borgmatic_manifest_path], {}, @@ -38,14 +40,14 @@ def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version): return manifest_data['config_paths'] - - def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version): ''' Run the "bootstrap" action for the given repository. ''' - manifest_config_paths = get_config_paths(bootstrap_arguments, global_arguments, local_borg_version) + manifest_config_paths = get_config_paths( + bootstrap_arguments, global_arguments, local_borg_version + ) for config_path in manifest_config_paths: logger.info('Bootstrapping config path %s', config_path) @@ -58,7 +60,7 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version): bootstrap_arguments.archive, {}, local_borg_version, - global_arguments + global_arguments, ), [config_path], {}, @@ -70,7 +72,3 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version): strip_components=bootstrap_arguments.strip_components, progress=bootstrap_arguments.progress, ) - - - - diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index a7667c31..76039313 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -1,5 +1,6 @@ import argparse import collections +import itertools from argparse import Action, ArgumentParser from borgmatic.config import collect @@ -75,14 +76,20 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): remaining_arguments.remove(item) try: - arguments[canonical_name] = None if canonical_name in subcommand_parsers_mapping else parsed + arguments[canonical_name] = ( + None if canonical_name in subcommand_parsers_mapping else parsed + ) except UnboundLocalError: pass - + for argument in arguments: if not arguments[argument]: - if not any(subcommand in arguments for subcommand in subcommand_parsers_mapping[argument]): - raise ValueError(f"Missing subcommand for {argument}. Expected one of {subcommand_parsers_mapping[argument]}") + if not any( + subcommand in arguments for subcommand in subcommand_parsers_mapping[argument] + ): + raise ValueError( + f'Missing subcommand for {argument}. Expected one of {subcommand_parsers_mapping[argument]}' + ) # If no actions are explicitly requested, assume defaults. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: @@ -93,6 +100,10 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): remaining_arguments = list(unparsed_arguments) + # Now ask each subparser, one by one, to greedily consume arguments, from last to first. This + # allows subparsers to consume arguments before their parent subparsers do. + remaining_subparser_arguments = [] + # Now ask each subparser, one by one, to greedily consume arguments, from last to first. This # allows subparsers to consume arguments before their parent subparsers do. for subparser_name, subparser in reversed(subparsers.items()): @@ -100,9 +111,23 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): continue subparser = subparsers[subparser_name] - unused_parsed, remaining_arguments = subparser.parse_known_args( - [argument for argument in remaining_arguments if argument != subparser_name] + unused_parsed, remaining = subparser.parse_known_args( + [argument for argument in unparsed_arguments if argument != subparser_name] ) + remaining_subparser_arguments.append(remaining) + + # Determine the remaining arguments that no subparsers have consumed. + if remaining_subparser_arguments: + remaining_arguments = [ + argument + for argument in dict.fromkeys( + itertools.chain.from_iterable(remaining_subparser_arguments) + ).keys() + if all( + argument in subparser_arguments + for subparser_arguments in remaining_subparser_arguments + ) + ] # Special case: If "borg" is present in the arguments, consume all arguments after (+1) the # "borg" action. @@ -551,9 +576,7 @@ def make_parsers(): ) config_group = config_parser.add_argument_group('config arguments') - config_group.add_argument( - '-h', '--help', action='help', help='Show this help message and exit' - ) + config_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') config_subparsers = config_parser.add_subparsers( title='config subcommands', @@ -568,7 +591,9 @@ def make_parsers(): description='Extract just the config files that were used to create a borgmatic repository during the "create" operation', add_help=False, ) - config_bootstrap_group = config_bootstrap_parser.add_argument_group('config bootstrap arguments') + config_bootstrap_group = config_bootstrap_parser.add_argument_group( + 'config bootstrap arguments' + ) config_bootstrap_group.add_argument( '--repository', help='Path of repository to extract config files from', @@ -579,7 +604,9 @@ def make_parsers(): help='Path that stores the config files used to create an archive, and additional source files used for temporary internal state like borgmatic database dumps. Defaults to ~/.borgmatic', ) config_bootstrap_group.add_argument( - '--archive', help='Name of archive to extract config files from, defaults to "latest"', default='latest' + '--archive', + help='Name of archive to extract config files from, defaults to "latest"', + default='latest', ) config_bootstrap_group.add_argument( '--destination', @@ -947,7 +974,9 @@ def make_parsers(): ) borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') - merged_subparsers = argparse._SubParsersAction(None, None, metavar=None, dest='merged', parser_class=None) + merged_subparsers = argparse._SubParsersAction( + None, None, metavar=None, dest='merged', parser_class=None + ) for name, subparser in subparsers.choices.items(): merged_subparsers._name_parser_map[name] = subparser @@ -957,7 +986,7 @@ def make_parsers(): merged_subparsers._name_parser_map[name] = subparser subparser._name_parser_map = merged_subparsers._name_parser_map - return top_level_parser, merged_subparsers + return top_level_parser, merged_subparsers def parse_arguments(*unparsed_arguments): @@ -967,12 +996,15 @@ def parse_arguments(*unparsed_arguments): ''' top_level_parser, subparsers = make_parsers() - arguments, remaining_arguments = parse_subparser_arguments( unparsed_arguments, subparsers.choices ) - if 'bootstrap' in arguments.keys() and 'config' in arguments.keys() and len(arguments.keys()) > 2: + if ( + 'bootstrap' in arguments.keys() + and 'config' in arguments.keys() + and len(arguments.keys()) > 2 + ): raise ValueError( 'The bootstrap action cannot be combined with other actions. Please run it separately.' ) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index ddfb9898..b3a86d98 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -18,10 +18,10 @@ import borgmatic.actions.borg import borgmatic.actions.break_lock import borgmatic.actions.check import borgmatic.actions.compact +import borgmatic.actions.config.bootstrap import borgmatic.actions.create import borgmatic.actions.export_tar import borgmatic.actions.extract -import borgmatic.actions.config.bootstrap import borgmatic.actions.info import borgmatic.actions.list import borgmatic.actions.mount @@ -622,12 +622,14 @@ def collect_configuration_run_summary_logs(configs, arguments): except ValueError as error: yield from log_error_records(str(error)) return - + if 'bootstrap' in arguments: # no configuration file is needed for bootstrap local_borg_version = borg_version.local_borg_version({}, 'borg') try: - borgmatic.actions.config.bootstrap.run_bootstrap(arguments['bootstrap'], arguments['global'], local_borg_version) + borgmatic.actions.config.bootstrap.run_bootstrap( + arguments['bootstrap'], arguments['global'], local_borg_version + ) yield logging.makeLogRecord( dict( levelno=logging.INFO,