|
@@ -1870,6 +1870,126 @@ class Archiver:
|
|
|
print(warning, file=sys.stderr)
|
|
|
return args
|
|
|
|
|
|
+ class CommonOptions:
|
|
|
+ """
|
|
|
+ Support class to allow specifying common options directly after the top-level command.
|
|
|
+
|
|
|
+ Normally options can only be specified on the parser defining them, which means
|
|
|
+ that generally speaking *all* options go after all sub-commands. This is annoying
|
|
|
+ for common options in scripts, e.g. --remote-path or logging options.
|
|
|
+
|
|
|
+ This class allows adding the same set of options to both the top-level parser
|
|
|
+ and the final sub-command parsers (but not intermediary sub-commands, at least for now).
|
|
|
+
|
|
|
+ It does so by giving every option's target name ("dest") a suffix indicating its level
|
|
|
+ -- no two options in the parser hierarchy can have the same target --
|
|
|
+ then, after parsing the command line, multiple definitions are resolved.
|
|
|
+
|
|
|
+ Defaults are handled by only setting them on the top-level parser and setting
|
|
|
+ a sentinel object in all sub-parsers, which then allows to discern which parser
|
|
|
+ supplied the option.
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, define_common_options, suffix_precedence):
|
|
|
+ """
|
|
|
+ *define_common_options* should be a callable taking one argument, which
|
|
|
+ will be a argparse.Parser.add_argument-like function.
|
|
|
+
|
|
|
+ *define_common_options* will be called multiple times, and should call
|
|
|
+ the passed function to define common options exactly the same way each time.
|
|
|
+
|
|
|
+ *suffix_precedence* should be a tuple of the suffixes that will be used.
|
|
|
+ It is ordered from lowest precedence to highest precedence:
|
|
|
+ An option specified on the parser belonging to index 0 is overridden if the
|
|
|
+ same option is specified on any parser with a higher index.
|
|
|
+ """
|
|
|
+ self.define_common_options = define_common_options
|
|
|
+ self.suffix_precedence = suffix_precedence
|
|
|
+
|
|
|
+ # Maps suffixes to sets of target names.
|
|
|
+ # E.g. common_options["_subcommand"] = {..., "log_level", ...}
|
|
|
+ self.common_options = dict()
|
|
|
+ # Set of options with the 'append' action.
|
|
|
+ self.append_options = set()
|
|
|
+ # This is the sentinel object that replaces all default values in parsers
|
|
|
+ # below the top-level parser.
|
|
|
+ self.default_sentinel = object()
|
|
|
+
|
|
|
+ def add_common_group(self, parser, suffix, provide_defaults=False):
|
|
|
+ """
|
|
|
+ Add common options to *parser*.
|
|
|
+
|
|
|
+ *provide_defaults* must only be True exactly once in a parser hierarchy,
|
|
|
+ at the top level, and False on all lower levels. The default is chosen
|
|
|
+ accordingly.
|
|
|
+
|
|
|
+ *suffix* indicates the suffix to use internally. It also indicates
|
|
|
+ which precedence the *parser* has for common options. See *suffix_precedence*
|
|
|
+ of __init__.
|
|
|
+ """
|
|
|
+ assert suffix in self.suffix_precedence
|
|
|
+
|
|
|
+ def add_argument(*args, **kwargs):
|
|
|
+ if 'dest' in kwargs:
|
|
|
+ kwargs.setdefault('action', 'store')
|
|
|
+ assert kwargs['action'] in ('help', 'store_const', 'store_true', 'store_false', 'store', 'append')
|
|
|
+ is_append = kwargs['action'] == 'append'
|
|
|
+ if is_append:
|
|
|
+ self.append_options.add(kwargs['dest'])
|
|
|
+ assert kwargs['default'] == [], 'The default is explicitly constructed as an empty list in resolve()'
|
|
|
+ else:
|
|
|
+ self.common_options.setdefault(suffix, set()).add(kwargs['dest'])
|
|
|
+ kwargs['dest'] += suffix
|
|
|
+ if not provide_defaults and 'default' in kwargs:
|
|
|
+ # Interpolate help now, in case the %(default)d (or so) is mentioned,
|
|
|
+ # to avoid producing incorrect help output.
|
|
|
+ # Assumption: Interpolated output can safely be interpolated again,
|
|
|
+ # which should always be the case.
|
|
|
+ # Note: We control all inputs.
|
|
|
+ kwargs['help'] = kwargs['help'] % kwargs
|
|
|
+ if not is_append:
|
|
|
+ kwargs['default'] = self.default_sentinel
|
|
|
+
|
|
|
+ common_group.add_argument(*args, **kwargs)
|
|
|
+
|
|
|
+ common_group = parser.add_argument_group('Common options')
|
|
|
+ self.define_common_options(add_argument)
|
|
|
+
|
|
|
+ def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict.
|
|
|
+ """
|
|
|
+ Resolve the multiple definitions of each common option to the final value.
|
|
|
+ """
|
|
|
+ for suffix in self.suffix_precedence:
|
|
|
+ # From highest level to lowest level, so the "most-specific" option wins, e.g.
|
|
|
+ # "borg --debug create --info" shall result in --info being effective.
|
|
|
+ for dest in self.common_options.get(suffix, []):
|
|
|
+ # map_from is this suffix' option name, e.g. log_level_subcommand
|
|
|
+ # map_to is the target name, e.g. log_level
|
|
|
+ map_from = dest + suffix
|
|
|
+ map_to = dest
|
|
|
+ # Retrieve value; depending on the action it may not exist, but usually does
|
|
|
+ # (store_const/store_true/store_false), either because the action implied a default
|
|
|
+ # or a default is explicitly supplied.
|
|
|
+ # Note that defaults on lower levels are replaced with default_sentinel.
|
|
|
+ # Only the top level has defaults.
|
|
|
+ value = getattr(args, map_from, self.default_sentinel)
|
|
|
+ if value is not self.default_sentinel:
|
|
|
+ # value was indeed specified on this level. Transfer value to target,
|
|
|
+ # and un-clobber the args (for tidiness - you *cannot* use the suffixed
|
|
|
+ # names for other purposes, obviously).
|
|
|
+ setattr(args, map_to, value)
|
|
|
+ delattr(args, map_from)
|
|
|
+
|
|
|
+ # Options with an "append" action need some special treatment. Instead of
|
|
|
+ # overriding values, all specified values are merged together.
|
|
|
+ for dest in self.append_options:
|
|
|
+ option_value = []
|
|
|
+ for suffix in self.suffix_precedence:
|
|
|
+ # Find values of this suffix, if any, and add them to the final list
|
|
|
+ values = getattr(args, dest + suffix, [])
|
|
|
+ option_value.extend(values)
|
|
|
+ setattr(args, dest, option_value)
|
|
|
+
|
|
|
def build_parser(self):
|
|
|
def process_epilog(epilog):
|
|
|
epilog = textwrap.dedent(epilog).splitlines()
|
|
@@ -1881,156 +2001,53 @@ class Archiver:
|
|
|
epilog = [line for line in epilog if not line.startswith('.. man')]
|
|
|
return '\n'.join(epilog)
|
|
|
|
|
|
- class CommonOptions:
|
|
|
- """
|
|
|
- Support class to allow specifying common options directly after the top-level command.
|
|
|
-
|
|
|
- Normally options can only be specified on the parser defining them, which means
|
|
|
- that generally speaking *all* options go after all sub-commands. This is annoying
|
|
|
- for common options in scripts, e.g. --remote-path or logging options.
|
|
|
-
|
|
|
- This class allows adding the same set of options to both the top-level parser
|
|
|
- and the final sub-command parsers (but not intermediary sub-commands, at least for now).
|
|
|
-
|
|
|
- It does so by giving every option's target name ("dest") a suffix indicating it's level
|
|
|
- -- no two options in the parser hierarchy can have the same target --
|
|
|
- then, after parsing the command line, multiple definitions are resolved.
|
|
|
-
|
|
|
- Defaults are handled by only setting them on the top-level parser and setting
|
|
|
- a sentinel object in all sub-parsers, which then allows to discern which parser
|
|
|
- supplied the option.
|
|
|
- """
|
|
|
-
|
|
|
- # From lowest precedence to highest precedence:
|
|
|
- # An option specified on the parser belonging to index 0 is overridden if the
|
|
|
- # same option is specified on any parser with a higher index.
|
|
|
- SUFFIX_PRECEDENCE = ('_maincommand', '_midcommand', '_subcommand')
|
|
|
-
|
|
|
- def __init__(self):
|
|
|
- from collections import defaultdict
|
|
|
-
|
|
|
- # Maps suffixes to sets of target names.
|
|
|
- # E.g. common_options["_subcommand"] = {..., "log_level", ...}
|
|
|
- self.common_options = defaultdict(defaultdict)
|
|
|
- self.append_options = set()
|
|
|
- self.default_sentinel = object()
|
|
|
-
|
|
|
- def add_common_group(self, parser, suffix='_subcommand', provide_defaults=False):
|
|
|
- """
|
|
|
- Add common options to *parser*.
|
|
|
-
|
|
|
- *provide_defaults* must only be True exactly once in a parser hierarchy,
|
|
|
- at the top level, and False on all lower levels. The default is chosen
|
|
|
- accordingly.
|
|
|
-
|
|
|
- *suffix* indicates the suffix to use internally. It also indicates
|
|
|
- which precedence the *parser* has for common options. See SUFFIX_PRECEDENCE.
|
|
|
- """
|
|
|
- assert suffix in self.SUFFIX_PRECEDENCE
|
|
|
-
|
|
|
- def add_argument(*args, **kwargs):
|
|
|
- if 'dest' in kwargs:
|
|
|
- is_append = kwargs.get('action') == 'append'
|
|
|
- if is_append:
|
|
|
- self.append_options.add(kwargs['dest'])
|
|
|
- assert kwargs['default'] == [], 'The default is explicitly constructed as an empty list in resolve()'
|
|
|
- else:
|
|
|
- self.common_options.setdefault(suffix, set()).add(kwargs['dest'])
|
|
|
- kwargs['dest'] += suffix
|
|
|
- if not provide_defaults and 'default' in kwargs:
|
|
|
- # Interpolate help now, in case the %(default)d (or so) is mentioned,
|
|
|
- # to avoid producing incorrect help output.
|
|
|
- # Assumption: Interpolated output can safely be interpolated again,
|
|
|
- # which should always be the case.
|
|
|
- # Note: We control all inputs.
|
|
|
- kwargs['help'] = kwargs['help'] % kwargs
|
|
|
- if not is_append:
|
|
|
- kwargs['default'] = self.default_sentinel
|
|
|
-
|
|
|
- common_group.add_argument(*args, **kwargs)
|
|
|
-
|
|
|
- common_group = parser.add_argument_group('Common options')
|
|
|
-
|
|
|
- add_argument('-h', '--help', action='help', help='show this help message and exit')
|
|
|
- add_argument('--critical', dest='log_level',
|
|
|
- action='store_const', const='critical', default='warning',
|
|
|
- help='work on log level CRITICAL')
|
|
|
- add_argument('--error', dest='log_level',
|
|
|
- action='store_const', const='error', default='warning',
|
|
|
- help='work on log level ERROR')
|
|
|
- add_argument('--warning', dest='log_level',
|
|
|
- action='store_const', const='warning', default='warning',
|
|
|
- help='work on log level WARNING (default)')
|
|
|
- add_argument('--info', '-v', '--verbose', dest='log_level',
|
|
|
- action='store_const', const='info', default='warning',
|
|
|
- help='work on log level INFO')
|
|
|
- add_argument('--debug', dest='log_level',
|
|
|
- action='store_const', const='debug', default='warning',
|
|
|
- help='enable debug output, work on log level DEBUG')
|
|
|
- add_argument('--debug-topic', dest='debug_topics',
|
|
|
- action='append', metavar='TOPIC', default=[],
|
|
|
- help='enable TOPIC debugging (can be specified multiple times). '
|
|
|
- 'The logger path is borg.debug.<TOPIC> if TOPIC is not fully qualified.')
|
|
|
- add_argument('-p', '--progress', dest='progress', action='store_true',
|
|
|
- help='show progress information')
|
|
|
- add_argument('--log-json', dest='log_json', action='store_true',
|
|
|
- help='Output one JSON object per log line instead of formatted text.')
|
|
|
- add_argument('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1,
|
|
|
- help='wait for the lock, but max. N seconds (default: %(default)d).')
|
|
|
- add_argument('--show-version', dest='show_version', action='store_true', default=False,
|
|
|
- help='show/log the borg version')
|
|
|
- add_argument('--show-rc', dest='show_rc', action='store_true', default=False,
|
|
|
- help='show/log the return code (rc)')
|
|
|
- add_argument('--no-files-cache', dest='cache_files', action='store_false',
|
|
|
- help='do not load/update the file metadata cache used to detect unchanged files')
|
|
|
- add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M',
|
|
|
- help='set umask to M (local and remote, default: %(default)04o)')
|
|
|
- add_argument('--remote-path', dest='remote_path', metavar='PATH',
|
|
|
- help='use PATH as borg executable on the remote (default: "borg")')
|
|
|
- add_argument('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate',
|
|
|
- help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)')
|
|
|
- add_argument('--consider-part-files', dest='consider_part_files',
|
|
|
- action='store_true', default=False,
|
|
|
- help='treat part files like normal files (e.g. to list/extract them)')
|
|
|
-
|
|
|
- def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict.
|
|
|
- """
|
|
|
- Resolve the multiple definitions of each common option to the final value.
|
|
|
- """
|
|
|
- for suffix in self.SUFFIX_PRECEDENCE:
|
|
|
- # From highest level to lowest level, so the "most-specific" option wins, e.g.
|
|
|
- # "borg --debug create --info" shall result in --info being effective.
|
|
|
- for dest in self.common_options.get(suffix, []):
|
|
|
- # map_from is this suffix' option name, e.g. log_level_subcommand
|
|
|
- # map_to is the target name, e.g. log_level
|
|
|
- map_from = dest + suffix
|
|
|
- map_to = dest
|
|
|
- # Retrieve value; depending on the action it may not exist, but usually does
|
|
|
- # (store_const/store_true/store_false), either because the action implied a default
|
|
|
- # or a default is explicitly supplied.
|
|
|
- # Note that defaults on lower levels are replaced with default_sentinel.
|
|
|
- # Only the top level has defaults.
|
|
|
- value = getattr(args, map_from, self.default_sentinel)
|
|
|
- if value is not self.default_sentinel:
|
|
|
- # value was indeed specified on this level. Transfer value to target,
|
|
|
- # and un-clobber the args (for tidiness - you *cannot* use the suffixed
|
|
|
- # names for other purposes, obviously).
|
|
|
- setattr(args, map_to, value)
|
|
|
- delattr(args, map_from)
|
|
|
-
|
|
|
- # Options with an "append" action need some special treatment. Instead of
|
|
|
- # overriding values, all specified values are merged together.
|
|
|
- for dest in self.append_options:
|
|
|
- option_value = []
|
|
|
- for suffix in self.SUFFIX_PRECEDENCE:
|
|
|
- # Find values of this suffix, if any, and add them to the final list
|
|
|
- values = getattr(args, dest + suffix, [])
|
|
|
- option_value.extend(values)
|
|
|
- setattr(args, dest, option_value)
|
|
|
+ def define_common_options(add_common_option):
|
|
|
+ add_common_option('-h', '--help', action='help', help='show this help message and exit')
|
|
|
+ add_common_option('--critical', dest='log_level',
|
|
|
+ action='store_const', const='critical', default='warning',
|
|
|
+ help='work on log level CRITICAL')
|
|
|
+ add_common_option('--error', dest='log_level',
|
|
|
+ action='store_const', const='error', default='warning',
|
|
|
+ help='work on log level ERROR')
|
|
|
+ add_common_option('--warning', dest='log_level',
|
|
|
+ action='store_const', const='warning', default='warning',
|
|
|
+ help='work on log level WARNING (default)')
|
|
|
+ add_common_option('--info', '-v', '--verbose', dest='log_level',
|
|
|
+ action='store_const', const='info', default='warning',
|
|
|
+ help='work on log level INFO')
|
|
|
+ add_common_option('--debug', dest='log_level',
|
|
|
+ action='store_const', const='debug', default='warning',
|
|
|
+ help='enable debug output, work on log level DEBUG')
|
|
|
+ add_common_option('--debug-topic', dest='debug_topics',
|
|
|
+ action='append', metavar='TOPIC', default=[],
|
|
|
+ help='enable TOPIC debugging (can be specified multiple times). '
|
|
|
+ 'The logger path is borg.debug.<TOPIC> if TOPIC is not fully qualified.')
|
|
|
+ add_common_option('-p', '--progress', dest='progress', action='store_true',
|
|
|
+ help='show progress information')
|
|
|
+ add_common_option('--log-json', dest='log_json', action='store_true',
|
|
|
+ help='Output one JSON object per log line instead of formatted text.')
|
|
|
+ add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1,
|
|
|
+ help='wait for the lock, but max. N seconds (default: %(default)d).')
|
|
|
+ add_common_option('--show-version', dest='show_version', action='store_true', default=False,
|
|
|
+ help='show/log the borg version')
|
|
|
+ add_common_option('--show-rc', dest='show_rc', action='store_true', default=False,
|
|
|
+ help='show/log the return code (rc)')
|
|
|
+ add_common_option('--no-files-cache', dest='cache_files', action='store_false',
|
|
|
+ help='do not load/update the file metadata cache used to detect unchanged files')
|
|
|
+ add_common_option('--umask', dest='umask', type=lambda s: int(s, 8), default=UMASK_DEFAULT, metavar='M',
|
|
|
+ help='set umask to M (local and remote, default: %(default)04o)')
|
|
|
+ add_common_option('--remote-path', dest='remote_path', metavar='PATH',
|
|
|
+ help='use PATH as borg executable on the remote (default: "borg")')
|
|
|
+ add_common_option('--remote-ratelimit', dest='remote_ratelimit', type=int, metavar='rate',
|
|
|
+ help='set remote network upload rate limit in kiByte/s (default: 0=unlimited)')
|
|
|
+ add_common_option('--consider-part-files', dest='consider_part_files',
|
|
|
+ action='store_true', default=False,
|
|
|
+ help='treat part files like normal files (e.g. to list/extract them)')
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog=self.prog, description='Borg - Deduplicated Backups',
|
|
|
add_help=False)
|
|
|
- parser.common_options = CommonOptions()
|
|
|
+ parser.common_options = self.CommonOptions(define_common_options,
|
|
|
+ suffix_precedence=('_maincommand', '_midcommand', '_subcommand'))
|
|
|
parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__,
|
|
|
help='show version number and exit')
|
|
|
parser.common_options.add_common_group(parser, '_maincommand', provide_defaults=True)
|