123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import collections
- from argparse import ArgumentParser
- from borgmatic.config import collect
- SUBPARSER_ALIASES = {
- 'init': ['--init', '-I'],
- 'prune': ['--prune', '-p'],
- 'create': ['--create', '-C'],
- 'check': ['--check', '-k'],
- 'extract': ['--extract', '-x'],
- 'list': ['--list', '-l'],
- 'info': ['--info', '-i'],
- }
- def parse_subparser_arguments(unparsed_arguments, subparsers):
- '''
- Given a sequence of arguments, and a subparsers object as returned by
- argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
- parsing all arguments. This allows common arguments like "--repository" to be shared across
- multiple subparsers.
- Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
- '''
- arguments = collections.OrderedDict()
- remaining_arguments = list(unparsed_arguments)
- alias_to_subparser_name = {
- alias: subparser_name
- for subparser_name, aliases in SUBPARSER_ALIASES.items()
- for alias in aliases
- }
- for subparser_name, subparser in subparsers.choices.items():
- if subparser_name not in remaining_arguments:
- continue
- canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
- # If a parsed value happens to be the same as the name of a subparser, remove it from the
- # remaining arguments. This prevents, for instance, "check --only extract" from triggering
- # the "extract" subparser.
- parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
- for value in vars(parsed).values():
- if isinstance(value, str):
- if value in subparsers.choices:
- remaining_arguments.remove(value)
- elif isinstance(value, list):
- for item in value:
- if item in subparsers.choices:
- remaining_arguments.remove(item)
- arguments[canonical_name] = parsed
- # If no actions are explicitly requested, assume defaults: prune, create, and check.
- if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
- for subparser_name in ('prune', 'create', 'check'):
- subparser = subparsers.choices[subparser_name]
- parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
- arguments[subparser_name] = parsed
- return arguments
- def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
- '''
- Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
- object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
- arguments as a parsed argparse.Namespace instance.
- '''
- # Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
- # are global arguments.
- remaining_arguments = list(unparsed_arguments)
- present_subparser_names = set()
- for subparser_name, subparser in subparsers.choices.items():
- if subparser_name not in remaining_arguments:
- continue
- present_subparser_names.add(subparser_name)
- unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
- # If no actions are explicitly requested, assume defaults: prune, create, and check.
- if (
- not present_subparser_names
- and '--help' not in unparsed_arguments
- and '-h' not in unparsed_arguments
- ):
- for subparser_name in ('prune', 'create', 'check'):
- subparser = subparsers.choices[subparser_name]
- unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
- # Remove the subparser names themselves.
- for subparser_name in present_subparser_names:
- if subparser_name in remaining_arguments:
- remaining_arguments.remove(subparser_name)
- return top_level_parser.parse_args(remaining_arguments)
- 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.
- '''
- config_paths = collect.get_default_config_paths()
- global_parser = ArgumentParser(add_help=False)
- global_group = global_parser.add_argument_group('global arguments')
- global_group.add_argument(
- '-c',
- '--config',
- nargs='*',
- dest='config_paths',
- default=config_paths,
- help='Configuration filenames or directories, defaults to: {}'.format(
- ' '.join(config_paths)
- ),
- )
- global_group.add_argument(
- '--excludes',
- dest='excludes_filename',
- help='Deprecated in favor of exclude_patterns within configuration',
- )
- global_group.add_argument(
- '-n',
- '--dry-run',
- dest='dry_run',
- action='store_true',
- help='Go through the motions, but do not actually write to any repositories',
- )
- global_group.add_argument(
- '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
- )
- global_group.add_argument(
- '-v',
- '--verbosity',
- type=int,
- choices=range(0, 3),
- default=0,
- help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
- )
- global_group.add_argument(
- '--syslog-verbosity',
- type=int,
- choices=range(0, 3),
- default=0,
- help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
- )
- global_group.add_argument(
- '--log-file',
- type=str,
- default=None,
- help='Write log messages to this file instead of concole and syslog',
- )
- global_group.add_argument(
- '--version',
- dest='version',
- default=False,
- action='store_true',
- help='Display installed version number of borgmatic and exit',
- )
- top_level_parser = ArgumentParser(
- description='''
- A simple wrapper script for the Borg backup software that creates and prunes backups.
- If none of the action options are given, then borgmatic defaults to: prune, create, and
- check archives.
- ''',
- parents=[global_parser],
- )
- subparsers = top_level_parser.add_subparsers(
- title='actions',
- metavar='',
- help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:',
- )
- init_parser = subparsers.add_parser(
- 'init',
- aliases=SUBPARSER_ALIASES['init'],
- help='Initialize an empty Borg repository',
- description='Initialize an empty Borg repository',
- add_help=False,
- )
- init_group = init_parser.add_argument_group('init arguments')
- init_group.add_argument(
- '-e',
- '--encryption',
- dest='encryption_mode',
- help='Borg repository encryption mode',
- required=True,
- )
- init_group.add_argument(
- '--append-only',
- dest='append_only',
- action='store_true',
- help='Create an append-only repository',
- )
- init_group.add_argument(
- '--storage-quota',
- dest='storage_quota',
- help='Create a repository with a fixed storage quota',
- )
- init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- prune_parser = subparsers.add_parser(
- 'prune',
- aliases=SUBPARSER_ALIASES['prune'],
- help='Prune archives according to the retention policy',
- description='Prune archives according to the retention policy',
- add_help=False,
- )
- prune_group = prune_parser.add_argument_group('prune arguments')
- prune_group.add_argument(
- '--stats',
- dest='stats',
- default=False,
- action='store_true',
- help='Display statistics of archive',
- )
- prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- create_parser = subparsers.add_parser(
- 'create',
- aliases=SUBPARSER_ALIASES['create'],
- help='Create archives (actually perform backups)',
- description='Create archives (actually perform backups)',
- add_help=False,
- )
- create_group = create_parser.add_argument_group('create arguments')
- create_group.add_argument(
- '--progress',
- dest='progress',
- default=False,
- action='store_true',
- help='Display progress for each file as it is processed',
- )
- create_group.add_argument(
- '--stats',
- dest='stats',
- default=False,
- action='store_true',
- help='Display statistics of archive',
- )
- create_group.add_argument(
- '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
- )
- create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- check_parser = subparsers.add_parser(
- 'check',
- aliases=SUBPARSER_ALIASES['check'],
- help='Check archives for consistency',
- description='Check archives for consistency',
- add_help=False,
- )
- check_group = check_parser.add_argument_group('check arguments')
- check_group.add_argument(
- '--only',
- metavar='CHECK',
- choices=('repository', 'archives', 'data', 'extract'),
- dest='only',
- action='append',
- help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
- )
- check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- extract_parser = subparsers.add_parser(
- 'extract',
- aliases=SUBPARSER_ALIASES['extract'],
- help='Extract a named archive to the current directory',
- description='Extract a named archive to the current directory',
- add_help=False,
- )
- extract_group = extract_parser.add_argument_group('extract arguments')
- extract_group.add_argument(
- '--repository',
- help='Path of repository to extract, defaults to the configured repository if there is only one',
- )
- extract_group.add_argument('--archive', help='Name of archive to operate on', required=True)
- extract_group.add_argument(
- '--restore-path',
- nargs='+',
- dest='restore_paths',
- help='Paths to restore from archive, defaults to the entire archive',
- )
- extract_group.add_argument(
- '--progress',
- dest='progress',
- default=False,
- action='store_true',
- help='Display progress for each file as it is processed',
- )
- extract_group.add_argument(
- '-h', '--help', action='help', help='Show this help message and exit'
- )
- list_parser = subparsers.add_parser(
- 'list',
- aliases=SUBPARSER_ALIASES['list'],
- help='List archives',
- description='List archives or the contents of an archive',
- add_help=False,
- )
- list_group = list_parser.add_argument_group('list arguments')
- list_group.add_argument(
- '--repository',
- help='Path of repository to list, defaults to the configured repository if there is only one',
- )
- list_group.add_argument('--archive', help='Name of archive to list')
- list_group.add_argument(
- '--short', default=False, action='store_true', help='Output only archive or path names'
- )
- list_group.add_argument('--format', help='Format for file listing')
- list_group.add_argument(
- '--json', default=False, action='store_true', help='Output results as JSON'
- )
- list_group.add_argument(
- '-P', '--prefix', help='Only list archive names starting with this prefix'
- )
- list_group.add_argument(
- '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
- )
- list_group.add_argument(
- '--successful',
- default=False,
- action='store_true',
- help='Only list archive names of successful (non-checkpoint) backups',
- )
- list_group.add_argument(
- '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
- )
- list_group.add_argument(
- '--first', metavar='N', help='List first N archives after other filters are applied'
- )
- list_group.add_argument(
- '--last', metavar='N', help='List last N archives after other filters are applied'
- )
- list_group.add_argument(
- '-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
- )
- list_group.add_argument(
- '--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
- )
- list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
- list_group.add_argument(
- '--patterns-from',
- metavar='FILENAME',
- help='Include or exclude paths matching patterns from pattern file, one per line',
- )
- list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- info_parser = subparsers.add_parser(
- 'info',
- aliases=SUBPARSER_ALIASES['info'],
- help='Display summary information on archives',
- description='Display summary information on archives',
- add_help=False,
- )
- info_group = info_parser.add_argument_group('info arguments')
- info_group.add_argument(
- '--repository',
- help='Path of repository to show info for, defaults to the configured repository if there is only one',
- )
- info_group.add_argument('--archive', help='Name of archive to show info for')
- info_group.add_argument(
- '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
- )
- info_group.add_argument(
- '-P', '--prefix', help='Only show info for archive names starting with this prefix'
- )
- info_group.add_argument(
- '-a',
- '--glob-archives',
- metavar='GLOB',
- help='Only show info for archive names matching this glob',
- )
- info_group.add_argument(
- '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
- )
- info_group.add_argument(
- '--first',
- metavar='N',
- help='Show info for first N archives after other filters are applied',
- )
- info_group.add_argument(
- '--last', metavar='N', help='Show info for first N archives after other filters are applied'
- )
- info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
- arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
- arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
- if arguments['global'].excludes_filename:
- raise ValueError(
- 'The --excludes option has been replaced with exclude_patterns in configuration'
- )
- if 'init' in arguments and arguments['global'].dry_run:
- raise ValueError('The init action cannot be used with the --dry-run option')
- if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful:
- raise ValueError('The --glob-archives and --successful options cannot be used together')
- if (
- 'list' in arguments
- and 'info' in arguments
- and arguments['list'].json
- and arguments['info'].json
- ):
- raise ValueError('With the --json option, list and info actions cannot be used together')
- return arguments
|