Browse Source

Initial stab at subparsers for argument parsing. Not yet fully working.

Dan Helfman 6 years ago
parent
commit
8c72e909a7
1 changed files with 228 additions and 126 deletions
  1. 228 126
      borgmatic/commands/borgmatic.py

+ 228 - 126
borgmatic/commands/borgmatic.py

@@ -26,6 +26,74 @@ from borgmatic.verbosity import verbosity_to_log_level
 logger = logging.getLogger(__name__)
 
 LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
+SUBPARSER_ALIASES = {
+    'init': ['--init', '-I'],
+    'prune': ['--prune', '-p'],
+    'create': ['--create', '-C'],
+    'check': ['--check', '-k'],
+    'extract': ['--extract', '-x'],
+    'list': ['--list', '-l'],
+    'info': ['--info', '-i'],
+}
+
+
+def split_arguments_by_subparser(arguments, subparsers):
+    '''
+    Parse out the arguments destined for each subparser. Also parse out global arguments not
+    destined for a particular subparser.
+
+    More specifically, given a sequence of arguments and a subparsers object as returned by
+    argparse.ArgumentParser().add_subparsers(), split the arguments on subparser names. Return the
+    result as a dict mapping from subparser name to the arguments for that subparser. This includes
+    a special subparser named "global" for global arguments.
+    '''
+    subparser_arguments = collections.defaultdict(list)
+    subparser_name = 'global'
+
+    for argument in arguments:
+        subparser = subparsers.choices.get(argument)
+
+        if subparser is None:
+            subparser_arguments[subparser_name].append(argument)
+        else:
+            subparser_name = argument
+            subparser_arguments[subparser_name] = []
+
+    return subparser_arguments
+
+
+def parse_subparser_arguments(subparser_arguments, top_level_parser, subparsers):
+    '''
+    Given a dict mapping from subparser name to the arguments for that subparser, a top-level parser
+    (containing subparsers), and a subparsers object as returned by
+    argparse.ArgumentParser().add_subparsers(), ask each subparser to parse its own arguments and
+    the top-level parser to parse any remaining arguments.
+
+    Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
+    arguments.
+    '''
+    parsed_arguments = collections.OrderedDict()
+    global_arguments = subparser_arguments['global']
+    alias_to_subparser_name = {
+        alias: subparser_name
+        for subparser_name, aliases in SUBPARSER_ALIASES.items()
+        for alias in aliases
+    }
+
+    for subparser_name, arguments in subparser_arguments.items():
+        if subparser_name == 'global':
+            continue
+
+        canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
+        subparser = subparsers.choices.get(canonical_name)
+
+        parsed, remaining = subparser.parse_known_args(arguments)
+        parsed_arguments[canonical_name] = parsed
+        global_arguments.extend(remaining)
+
+    parsed_arguments['global'] = top_level_parser.parse_args(global_arguments)
+
+    return parsed_arguments
 
 
 def parse_arguments(*arguments):
@@ -35,58 +103,78 @@ def parse_arguments(*arguments):
     '''
     config_paths = collect.get_default_config_paths()
 
-    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.
-            ''',
-        add_help=False,
-    )
+    global_parser = ArgumentParser(add_help=False)
+    global_group = global_parser.add_argument_group('global arguments')
 
-    actions_group = parser.add_argument_group('actions')
-    actions_group.add_argument(
-        '-I', '--init', dest='init', action='store_true', help='Initialize an empty Borg repository'
+    global_group.add_argument(
+        '-c',
+        '--config',
+        nargs='*',
+        dest='config_paths',
+        default=config_paths,
+        help='Configuration filenames or directories, defaults to: {}'.format(
+            ' '.join(config_paths)
+        ),
     )
-    actions_group.add_argument(
-        '-p',
-        '--prune',
-        dest='prune',
-        action='store_true',
-        help='Prune archives according to the retention policy',
+    global_group.add_argument(
+        '--excludes',
+        dest='excludes_filename',
+        help='Deprecated in favor of exclude_patterns within configuration',
     )
-    actions_group.add_argument(
-        '-C',
-        '--create',
-        dest='create',
+    global_group.add_argument(
+        '-n',
+        '--dry-run',
+        dest='dry_run',
         action='store_true',
-        help='Create archives (actually perform backups)',
+        help='Go through the motions, but do not actually write to any repositories',
     )
-    actions_group.add_argument(
-        '-k', '--check', dest='check', action='store_true', help='Check archives for consistency'
+    global_group.add_argument(
+        '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
     )
-
-    actions_group.add_argument(
-        '-x',
-        '--extract',
-        dest='extract',
-        action='store_true',
-        help='Extract a named archive to the current directory',
+    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)',
     )
-    actions_group.add_argument(
-        '-l', '--list', dest='list', action='store_true', help='List archives'
+    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)',
     )
-    actions_group.add_argument(
-        '-i',
-        '--info',
-        dest='info',
+    global_group.add_argument(
+        '--version',
+        dest='version',
+        default=False,
         action='store_true',
-        help='Display summary information on archives',
+        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],
     )
 
-    init_group = parser.add_argument_group('options for --init')
+    subparsers = top_level_parser.add_subparsers(title='actions', metavar='')
+    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'
+        '-e', '--encryption', dest='encryption_mode', help='Borg repository encryption mode',
+        required=True,
     )
     init_group.add_argument(
         '--append-only',
@@ -99,132 +187,146 @@ def parse_arguments(*arguments):
         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_group = parser.add_argument_group('options for --prune')
-    stats_argument = prune_group.add_argument(
+    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_group = parser.add_argument_group('options for --create')
-    progress_argument = create_group.add_argument(
+    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._group_actions.append(stats_argument)
-    json_argument = create_group.add_argument(
+    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(
+        '-h', '--help', action='help', help='Show this help message and exit'
+    )
 
-    extract_group = parser.add_argument_group('options for --extract')
-    repository_argument = extract_group.add_argument(
+    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 use, defaults to the configured repository if there is only one',
     )
-    archive_argument = extract_group.add_argument('--archive', help='Name of archive to operate on')
+    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._group_actions.append(progress_argument)
-
-    list_group = parser.add_argument_group('options for --list')
-    list_group._group_actions.append(repository_argument)
-    list_group._group_actions.append(archive_argument)
-    list_group._group_actions.append(json_argument)
-
-    info_group = parser.add_argument_group('options for --info')
-    info_group._group_actions.append(json_argument)
+    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'
+    )
 
-    common_group = parser.add_argument_group('common options')
-    common_group.add_argument(
-        '-c',
-        '--config',
-        nargs='+',
-        dest='config_paths',
-        default=config_paths,
-        help='Configuration filenames or directories, defaults to: {}'.format(
-            ' '.join(config_paths)
-        ),
+    list_parser = subparsers.add_parser(
+        'list', aliases=SUBPARSER_ALIASES['list'], help='List archives', description='List archives',
+        add_help=False,
     )
-    common_group.add_argument(
-        '--excludes',
-        dest='excludes_filename',
-        help='Deprecated in favor of exclude_patterns within configuration',
+    list_group = list_parser.add_argument_group('list arguments')
+    list_group.add_argument(
+        '--repository',
+        help='Path of repository to use, defaults to the configured repository if there is only one',
     )
-    common_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',
+    list_group.add_argument(
+        '--archive', help='Name of archive to operate on'
     )
-    common_group.add_argument(
-        '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
+    list_group.add_argument(
+        '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
     )
-    common_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)',
+    list_group.add_argument(
+        '-h', '--help', action='help', help='Show this help message and exit'
     )
-    common_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)',
+
+    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,
     )
-    common_group.add_argument(
-        '--version',
-        dest='version',
-        default=False,
-        action='store_true',
-        help='Display installed version number of borgmatic and exit',
+    info_group = info_parser.add_argument_group('info arguments')
+    info_group.add_argument(
+        '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
+    )
+    info_group.add_argument(
+        '-h', '--help', action='help', help='Show this help message and exit'
     )
-    common_group.add_argument('--help', action='help', help='Show this help information and exit')
 
-    args = parser.parse_args(arguments)
+    subparser_arguments = split_arguments_by_subparser(arguments, subparsers)
+    parsed_arguments = parse_subparser_arguments(subparser_arguments, top_level_parser, subparsers)
 
-    if args.excludes_filename:
+    if parsed_arguments.excludes_filename:
         raise ValueError(
             'The --excludes option has been replaced with exclude_patterns in configuration'
         )
 
-    if (args.encryption_mode or args.append_only or args.storage_quota) and not args.init:
-        raise ValueError(
-            'The --encryption, --append-only, and --storage-quota options can only be used with the --init option'
-        )
-
-    if args.init and args.dry_run:
-        raise ValueError('The --init option cannot be used with the --dry-run option')
-    if args.init and not args.encryption_mode:
-        raise ValueError('The --encryption option is required with the --init option')
-
-    if not args.extract:
-        if not args.list:
-            if args.repository:
-                raise ValueError(
-                    'The --repository option can only be used with the --extract and --list options'
-                )
-            if args.archive:
-                raise ValueError(
-                    'The --archive option can only be used with the --extract and --list options'
-                )
-        if args.restore_paths:
-            raise ValueError('The --restore-path option can only be used with the --extract option')
-    if args.extract and not args.archive:
-        raise ValueError('The --archive option is required with the --extract option')
+    if 'init' in parsed_arguments and parsed_arguments['global'].dry_run:
+        raise ValueError('The init action cannot be used with the --dry-run option')
 
     if args.progress and not (args.create or args.extract):
         raise ValueError(