Merge pull request #9413 from ThomasWaldmann/jsonargparse2
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.7) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run

use jsonargparse
This commit is contained in:
TW 2026-03-10 23:44:48 +01:00 committed by GitHub
commit 3d437d8589
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 820 additions and 937 deletions

View file

@ -89,6 +89,9 @@ Examples
$ find ~ -size -1000k | borg create --paths-from-stdin small-files-only
# Use --paths-from-command with find to back up files from only a given user
$ borg create --paths-from-command joes-files -- find /srv/samba/shared -user joe
# Use --paths-from-shell-command with find to back up a few files from only a given user -
# BE VERY CAREFUL AND ONLY USE TRUSTED INPUT FOR THE SHELL COMMAND!
$ borg create --paths-from-shell-command some-of-joes-files -- "find /srv/samba/shared -user joe | head"
# Use --paths-from-stdin with --paths-delimiter (for example, for filenames with newlines in them)
$ find ~ -size -1000k -print0 | borg create \
--paths-from-stdin \

View file

@ -0,0 +1,64 @@
Configuration Precedence
~~~~~~~~~~~~~~~~~~~~~~~~
From lowest to highest:
1. Defaults defined in the source code.
2. Default config file (``$BORG_CONFIG_DIR/default.yaml``).
3. ``--config`` file(s) (in the order given).
4. Full config environment variable: (``BORG_CONFIG``).
5. Environment variables (e.g. ``BORG_LOG_LEVEL``).
6. Command-line arguments in order left to right (might include config files).
Configuration files
~~~~~~~~~~~~~~~~~~~
Borg supports reading options from YAML configuration files. This is
implemented via `jsonargparse <https://jsonargparse.readthedocs.io/>`_
and works for all options that can also be set on the command line.
Default configuration file
``$BORG_CONFIG_DIR/default.yaml`` is loaded automatically on every Borg
invocation if it exists. You do not need to pass ``--config`` explicitly
for this file.
``--config PATH``
Load additional options from the YAML file at *PATH*.
Options in this file take precedence over the default config file but are
overridden by explicit command-line arguments. This option can be used
multiple times, with later files overriding earlier ones.
``--print_config``
Print the current effective configuration (all options in YAML format) to
stdout and exit. This reflects the merged result of the default config
file, any ``--config`` file, environment variables, and command-line
arguments given before ``--print_config``. The output can be used as a
starting point for a config file.
File format
Config files are YAML documents. Top-level keys are option names
(without leading ``--`` and with ``-`` replaced by ``_``).
Nested keys correspond to subcommands.
Example ``default.yaml``::
# apply to all borg commands:
log_level: info
show_rc: true
# options specific to "borg create":
create:
compression: zstd,3
stats: true
The top-level keys set options that are common to all commands (equivalent
to placing them before the subcommand on the command line). Keys nested
under a subcommand name (e.g. ``create:``) are only applied when that
subcommand is invoked.
.. note::
``--print_config`` shows the merged effective configuration and is a
convenient way to check what values Borg will actually use, and to
generate contents for your borg config file(s)::
borg --repo /backup/main create --compression zstd,3 --print_config

View file

@ -284,3 +284,29 @@ Please note:
.. _INI: https://docs.python.org/3/library/logging.config.html#configuration-file-format
.. _tempfile: https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir
Automatically generated Environment Variables (jsonargparse)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Borg uses jsonargparse_ with ``default_env=True``, which means that every
command-line option can also be set via an environment variable.
The environment variable name is derived from the program name (``borg``),
the subcommand (if any), and the option name, all converted to uppercase
with dashes replaced by underscores.
For **top-level options** (not specific to a subcommand), the pattern is::
BORG_<OPTION>
For example, ``--lock-wait`` can be set via ``BORG_LOCK_WAIT``.
For **subcommand options**, the subcommand and option are separated by a
double underscore::
BORG_<SUBCOMMAND>__<OPTION>
For example, ``borg create --comment`` can be set via ``BORG_CREATE__COMMENT``.
.. _jsonargparse: https://jsonargparse.readthedocs.io/

View file

@ -10,6 +10,10 @@
.. include:: general/return-codes.rst.inc
.. _config:
.. include:: general/config.rst.inc
.. _env_vars:
.. include:: general/environment.rst.inc

View file

@ -40,6 +40,8 @@ dependencies = [
"shtab>=1.8.0",
"backports-zstd; python_version < '3.14'", # for python < 3.14.
"xxhash>=2.0.0",
"jsonargparse @ https://github.com/omni-us/jsonargparse/zipball/main", # pull it from there until it is on pypi
"PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files
]
[project.optional-dependencies]
@ -259,7 +261,7 @@ deps = ["ruff"]
commands = [["ruff", "check", "."]]
[tool.tox.env.mypy]
deps = ["pytest", "mypy", "pkgconfig"]
deps = ["pytest", "mypy", "pkgconfig", "types-PyYAML"]
commands = [["mypy", "--ignore-missing-imports"]]
[tool.tox.env.docs]

View file

@ -14,3 +14,4 @@ pytest-cov==7.0.0
pytest-benchmark==5.2.3
Cython==3.2.4
pre-commit==4.5.1
types-PyYAML==6.0.12.20250915

View file

@ -15,3 +15,4 @@ pytest-benchmark
Cython
pre-commit
bandit[toml]
types-PyYAML

View file

@ -9,6 +9,7 @@ import textwrap
from collections import OrderedDict
from datetime import datetime, timezone
import time
import argparse # do not change to jsonargparse, shall not require 3rd party pkgs
def format_metavar(option):
@ -46,7 +47,7 @@ class BuildUsage:
is_subcommand = False
choices = {}
for action in parser._actions:
if action.choices is not None and "SubParsersAction" in str(action.__class__):
if action.choices is not None and "SubCommands" in str(action.__class__):
is_subcommand = True
for cmd, parser in action.choices.items():
choices[prefix + cmd] = parser
@ -100,17 +101,18 @@ class BuildUsage:
return is_subcommand
def write_usage(self, parser, fp):
if any(len(o.option_strings) for o in parser._actions):
actions = [o for o in parser._actions if getattr(o, "help", None) != argparse.SUPPRESS]
if any(len(o.option_strings) for o in actions):
fp.write(" [options]")
for option in parser._actions:
for option in actions:
if option.option_strings:
continue
fp.write(" " + format_metavar(option))
fp.write("\n\n")
def write_options(self, parser, fp):
def is_positional_group(group):
return any(not o.option_strings for o in group._group_actions)
def is_positional_group(actions):
return any(not o.option_strings for o in actions)
# HTML output:
# A table using some column-spans
@ -121,17 +123,18 @@ class BuildUsage:
# (no of columns used, columns, ...)
rows.append((1, ".. class:: borg-common-opt-ref\n\n:ref:`common_options`"))
else:
if not group._group_actions:
actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS]
if not actions:
continue
group_header = "**%s**" % group.title
if group.description:
group_header += "" + group.description
rows.append((1, group_header))
if is_positional_group(group):
for option in group._group_actions:
if is_positional_group(actions):
for option in actions:
rows.append((3, "", "``%s``" % option.metavar, option.help or ""))
else:
for option in group._group_actions:
for option in actions:
if option.metavar:
option_fmt = "``%s " + option.metavar + "``"
else:
@ -218,18 +221,19 @@ class BuildUsage:
)
def write_options_group(self, group, fp, with_title=True, base_indent=4):
def is_positional_group(group):
return any(not o.option_strings for o in group._group_actions)
def is_positional_group(actions):
return any(not o.option_strings for o in actions)
indent = " " * base_indent
actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS]
if is_positional_group(group):
for option in group._group_actions:
if is_positional_group(actions):
for option in actions:
fp.write(option.metavar + "\n")
fp.write(textwrap.indent(option.help or "", " " * base_indent) + "\n")
return
if not group._group_actions:
if not actions:
return
if with_title:
@ -238,7 +242,7 @@ class BuildUsage:
opts = OrderedDict()
for option in group._group_actions:
for option in actions:
if option.metavar:
option_fmt = "%s " + option.metavar
else:
@ -323,7 +327,7 @@ class BuildMan:
is_subcommand = False
choices = {}
for action in parser._actions:
if action.choices is not None and "SubParsersAction" in str(action.__class__):
if action.choices is not None and "SubCommands" in str(action.__class__):
is_subcommand = True
for cmd, parser in action.choices.items():
choices[prefix + cmd] = parser
@ -349,7 +353,7 @@ class BuildMan:
self.write_heading(write, "SYNOPSIS")
if is_intermediary:
subparsers = [action for action in parser._actions if "SubParsersAction" in str(action.__class__)][0]
subparsers = [action for action in parser._actions if "SubCommands" in str(action.__class__)][0]
for subcommand in subparsers.choices:
write("| borg", "[common options]", command, subcommand, "...")
self.see_also.setdefault(command, []).append(f"{command}-{subcommand}")
@ -503,34 +507,38 @@ class BuildMan:
fd.write(man_page)
def write_usage(self, write, parser):
if any(len(o.option_strings) for o in parser._actions):
actions = [o for o in parser._actions if getattr(o, "help", None) != argparse.SUPPRESS]
if any(len(o.option_strings) for o in actions):
write(" [options] ", end="")
for option in parser._actions:
for option in actions:
if option.option_strings:
continue
write(format_metavar(option), end=" ")
def write_options(self, write, parser):
for group in parser._action_groups:
if group.title == "Common options" or not group._group_actions:
actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS]
if group.title == "Common options" or not actions:
continue
title = "arguments" if group.title == "positional arguments" else group.title
self.write_heading(write, title, "+")
self.write_options_group(write, group)
def write_options_group(self, write, group):
def is_positional_group(group):
return any(not o.option_strings for o in group._group_actions)
def is_positional_group(actions):
return any(not o.option_strings for o in actions)
if is_positional_group(group):
for option in group._group_actions:
actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS]
if is_positional_group(actions):
for option in actions:
write(option.metavar)
write(textwrap.indent(option.help or "", " " * 4))
return
opts = OrderedDict()
for option in group._group_actions:
for option in actions:
if option.metavar:
option_fmt = "%s " + option.metavar
else:

View file

@ -25,7 +25,6 @@ from . import xattr
from .chunkers import get_chunker, Chunk
from .cache import ChunkListEntry, build_chunkindex_from_repo, delete_chunkindex_cache
from .crypto.key import key_factory, UnsupportedPayloadError
from .compress import CompressionSpec
from .constants import * # NOQA
from .crypto.low_level import IntegrityError as IntegrityErrorBase
from .helpers import BackupError, BackupRaceConditionError, BackupItemExcluded
@ -35,7 +34,7 @@ from .helpers import HardLinkManager
from .helpers import ChunkIteratorFileWrapper, open_item
from .helpers import Error, IntegrityError, set_ec
from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns
from .helpers import parse_timestamp, archive_ts_now
from .helpers import parse_timestamp, archive_ts_now, CompressionSpec
from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize
from .helpers import safe_encode, make_path_safe, remove_surrogates, text_to_json, join_cmd, remove_dotdot_prefixes
from .helpers import StableDict

View file

@ -15,7 +15,6 @@ else:
sys.exit(2) # == EXIT_ERROR
try:
import argparse
import faulthandler
import functools
import inspect
@ -40,12 +39,13 @@ try:
from ..helpers import format_file_size
from ..helpers import remove_surrogates, text_to_json
from ..helpers import DatetimeWrapper, replace_placeholders
from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, SUPPRESS
from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo
from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
from ..helpers import ErrorIgnoringTextIOWrapper
from ..helpers import msgpack
from ..helpers import sig_int
from ..helpers import get_config_dir
from ..remote import RemoteRepository
from ..selftest import selftest
except BaseException:
@ -63,18 +63,6 @@ STATS_HEADER = " Original size Deduplicated size"
PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance."
def get_func(args):
# This works around https://bugs.python.org/issue9351
# func is used at the leaf parsers of the argparse parser tree,
# fallback_func at next level towards the root,
# fallback2_func at the 2nd next level (which is root in our case).
for name in "func", "fallback_func", "fallback2_func":
func = getattr(args, name, None)
if func is not None:
return func
raise Exception("expected func attributes not found")
from .analyze_cmd import AnalyzeMixIn
from .benchmark_cmd import BenchmarkMixIn
from .check_cmd import CheckMixIn
@ -191,62 +179,45 @@ class Archiver(
class CommonOptions:
"""
Support class to allow specifying common options directly after the top-level command.
Support class to allow specifying common options at multiple levels of the command hierarchy.
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.
Common options (e.g. --log-level, --repo) can be placed anywhere in the command line:
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).
borg --info create ... # before the subcommand
borg create --info ... # after the subcommand
borg --info debug info --debug # at both levels of a two-level command
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.
Each parser level registers the same options with the same dest names.
Defaults are only provided on the top-level parser; all sub-parsers use SUPPRESS so
that unset options don't appear in the namespace at all.
Defaults are handled by only setting them on the top-level parser and setting
a sentinel object in all sub-parsers, which then allows one to discern which parser
supplied the option.
flatten_namespace() handles precedence: it walks sub-namespaces depth-first, so the
most-specific (innermost) value wins. For append-action options (e.g. --debug-topic)
it merges lists from all levels.
"""
def __init__(self, define_common_options, suffix_precedence):
def __init__(self, define_common_options):
"""
*define_common_options* should be a callable taking one argument, which
will be a argparse.Parser.add_argument-like function.
will be an 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):
def add_common_group(self, parser, 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__.
*provide_defaults* must be True exactly once in a parser hierarchy (the top-level
parser) and False on all sub-parsers. Sub-parsers get SUPPRESS as the default so
that an unspecified option produces no attribute, leaving the top-level default intact
after flatten_namespace() merges the namespaces.
"""
assert suffix in self.suffix_precedence
def add_argument(*args, **kwargs):
if "dest" in kwargs:
@ -261,97 +232,47 @@ class Archiver(
"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:
# Interpolate help now, in case the %(default)d (or so) is mentioned,
# Interpolate help now, in case %(default)d (or similar) 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
kwargs["default"] = SUPPRESS
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)
try:
delattr(args, map_from)
except AttributeError:
pass
# 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
extend_from = dest + suffix
if extend_from in args:
values = getattr(args, extend_from)
delattr(args, extend_from)
option_value.extend(values)
setattr(args, dest, option_value)
def build_parser(self):
from ._common import define_common_options
parser = argparse.ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False)
# paths and patterns must have an empty list as default everywhere
parser.set_defaults(fallback2_func=functools.partial(self.do_maincommand_help, parser), paths=[], patterns=[])
parser.common_options = self.CommonOptions(
define_common_options, suffix_precedence=("_maincommand", "_midcommand", "_subcommand")
parser = ArgumentParser(
prog=self.prog,
description="Borg - Deduplicated Backups",
default_config_files=[os.path.join(get_config_dir(), "default.yaml")],
default_env=True,
env_prefix="BORG",
)
parser.add_argument("--config", action="config")
# paths and patterns must have an empty list as default everywhere
parser.common_options = self.CommonOptions(define_common_options)
parser.add_argument(
"-V", "--version", action="version", version="%(prog)s " + __version__, help="show version number and exit"
)
parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI")
parser.common_options.add_common_group(parser, "_maincommand", provide_defaults=True)
parser.common_options.add_common_group(parser, provide_defaults=True)
common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog)
common_parser.set_defaults(paths=[], patterns=[])
parser.common_options.add_common_group(common_parser, "_subcommand")
common_parser = ArgumentParser(prog=self.prog)
parser.common_options.add_common_group(common_parser)
mid_common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog)
mid_common_parser.set_defaults(paths=[], patterns=[])
parser.common_options.add_common_group(mid_common_parser, "_midcommand")
mid_common_parser = ArgumentParser(prog=self.prog)
parser.common_options.add_common_group(mid_common_parser)
if parser.prog == "borgfs":
return self.build_parser_borgfs(parser)
subparsers = parser.add_subparsers(title="required arguments", metavar="<command>")
subparsers = parser.add_subcommands(required=False, title="required arguments", metavar="<command>")
self.build_parser_analyze(subparsers, common_parser, mid_common_parser)
self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
@ -424,8 +345,14 @@ class Archiver(
args = self.preprocess_args(args)
parser = self.build_parser()
args = parser.parse_args(args or ["-h"])
parser.common_options.resolve(args)
func = get_func(args)
args = flatten_namespace(args)
# Ensure list defaults previously handled by set_defaults are present
for list_attr in ("paths", "patterns", "pattern_roots"):
if getattr(args, list_attr, None) is None:
setattr(args, list_attr, [])
func = self.get_func(args, parser)
if func == self.do_create and args.paths and args.paths_from_stdin:
parser.error("Must not pass PATH with --paths-from-stdin.")
if args.progress and getattr(args, "output_list", False) and not args.log_json:
@ -433,8 +360,7 @@ class Archiver(
if func == self.do_create and not args.paths:
if args.content_from_command or args.paths_from_command:
parser.error("No command given.")
elif not args.paths_from_stdin:
# need at least 1 path but args.paths may also be populated from patterns
elif not args.paths_from_stdin and not args.pattern_roots:
parser.error("Need at least one PATH argument.")
# we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing,
# e.g. due to options like --timestamp that override the current time.
@ -452,8 +378,24 @@ class Archiver(
if value:
setattr(args, name, [replace_placeholders(elem) for elem in value])
args.func = func
return args
def get_func(self, args, parser):
if not getattr(args, "subcommand", None):
return functools.partial(self.do_maincommand_help, parser)
method_name = "do_" + args.subcommand.replace(" ", "_").replace("-", "_")
func = getattr(self, method_name, None)
if func is not None:
if method_name == "do_help":
return functools.partial(func, parser)
return func
# fallback to general help for e.g., "borg key"
return functools.partial(self.do_maincommand_help, parser)
def prerun_checks(self, logger, is_serve):
selftest(logger)
@ -486,7 +428,7 @@ class Archiver(
def run(self, args):
os.umask(args.umask) # early, before opening files
self.lock_wait = args.lock_wait
func = get_func(args)
func = args.func
# do not use loggers before this!
is_serve = func == self.do_serve
self.log_json = args.log_json and not is_serve
@ -633,7 +575,7 @@ def main(): # pragma: no cover
tb = format_tb(e)
print(tb, file=sys.stderr)
sys.exit(e.exit_code)
except argparse.ArgumentTypeError as e:
except ArgumentTypeError as e:
# we might not have logging setup yet, so get out quickly
print(str(e), file=sys.stderr)
sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR)

View file

@ -7,8 +7,9 @@ from ..archive import Archive
from ..constants import * # NOQA
from ..cache import Cache, assert_secure
from ..helpers import Error
from ..helpers import SortBySpec, positive_int_validator, location_validator, Location, relative_time_marker_validator
from ..helpers import Highlander
from ..helpers import SortBySpec, location_validator, Location, relative_time_marker_validator
from ..helpers import Highlander, octal_int
from ..helpers.argparsing import SUPPRESS, PositiveInt
from ..helpers.nanorst import rst_to_terminal
from ..manifest import Manifest, AI_HUMAN_SORT_KEYS
from ..patterns import PatternMatcher
@ -268,6 +269,8 @@ def process_epilog(epilog):
def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False):
add_option("--pattern-roots-internal", dest="pattern_roots", action="append", default=[], help=SUPPRESS)
add_option("--patterns-internal", dest="patterns", action="append", default=[], help=SUPPRESS)
add_option(
"-e",
"--exclude",
@ -275,6 +278,7 @@ def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components
dest="patterns",
type=parse_exclude_pattern,
action="append",
default=[],
help="exclude paths matching PATTERN",
)
add_option(
@ -371,8 +375,7 @@ def define_archive_filters_group(
"--first",
metavar="N",
dest="first",
type=positive_int_validator,
default=0,
type=PositiveInt,
action=Highlander,
help="consider the first N archives after other filters are applied",
)
@ -380,8 +383,7 @@ def define_archive_filters_group(
"--last",
metavar="N",
dest="last",
type=positive_int_validator,
default=0,
type=PositiveInt,
action=Highlander,
help="consider the last N archives after other filters are applied",
)
@ -508,7 +510,7 @@ def define_common_options(add_common_option):
"--umask",
metavar="M",
dest="umask",
type=lambda s: int(s, 8),
type=octal_int,
default=UMASK_DEFAULT,
action=Highlander,
help="set umask to M (local only, default: %(default)04o)",
@ -574,10 +576,11 @@ def define_common_options(add_common_option):
)
def build_matcher(inclexcl_patterns, include_paths):
def build_matcher(inclexcl_patterns, include_paths, pattern_roots=()):
matcher = PatternMatcher()
matcher.add_inclexcl(inclexcl_patterns)
matcher.add_includepaths(include_paths)
paths = list(pattern_roots) + list(include_paths)
matcher.add_includepaths(paths)
return matcher

View file

@ -1,4 +1,3 @@
import argparse
from collections import defaultdict
import os
@ -7,6 +6,7 @@ from ..archive import Archive
from ..constants import * # NOQA
from ..helpers import bin_to_hex, Error
from ..helpers import ProgressIndicatorPercent
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..remote import RemoteRepository
from ..repository import Repository
@ -126,14 +126,6 @@ class AnalyzeMixIn:
to recreate existing archives without them.
"""
)
subparser = subparsers.add_parser(
"analyze",
parents=[common_parser],
add_help=False,
description=self.do_analyze.__doc__,
epilog=analyze_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="analyze archives",
)
subparser.set_defaults(func=self.do_analyze)
subparser = ArgumentParser(parents=[common_parser], description=self.do_analyze.__doc__, epilog=analyze_epilog)
subparsers.add_subcommand("analyze", subparser, help="analyze archives")
define_archive_filters_group(subparser)

View file

@ -1,6 +1,4 @@
import argparse
from contextlib import contextmanager
import functools
import json
import logging
import os
@ -9,10 +7,11 @@ import time
from ..constants import * # NOQA
from ..crypto.key import FlexiKey
from ..helpers import format_file_size
from ..helpers import format_file_size, CompressionSpec
from ..helpers import json_print
from ..helpers import msgpack
from ..helpers import get_reset_ec
from ..helpers.argparsing import ArgumentParser
from ..item import Item
from ..platform import SyncFile
@ -303,8 +302,6 @@ class BenchmarkMixIn:
else:
print(f"{spec:<24} {number_kdf:<10} {dt:.3f}s")
from ..compress import CompressionSpec
if not args.json:
print("Compression ====================================================")
else:
@ -355,18 +352,12 @@ class BenchmarkMixIn:
benchmark_epilog = process_epilog("These commands do various benchmarks.")
subparser = subparsers.add_parser(
"benchmark",
parents=[mid_common_parser],
add_help=False,
description="benchmark command",
epilog=benchmark_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="benchmark command",
subparser = ArgumentParser(
parents=[mid_common_parser], description="benchmark command", epilog=benchmark_epilog
)
subparsers.add_subcommand("benchmark", subparser, help="benchmark command")
benchmark_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
benchmark_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
bench_crud_epilog = process_epilog(
"""
@ -409,16 +400,12 @@ class BenchmarkMixIn:
Try multiple measurements and having a otherwise idle machine (and network, if you use it).
"""
)
subparser = benchmark_parsers.add_parser(
"crud",
parents=[common_parser],
add_help=False,
description=self.do_benchmark_crud.__doc__,
epilog=bench_crud_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="benchmarks Borg CRUD (create, extract, update, delete).",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_benchmark_crud.__doc__, epilog=bench_crud_epilog
)
benchmark_parsers.add_subcommand(
"crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete)."
)
subparser.set_defaults(func=self.do_benchmark_crud)
subparser.add_argument("path", metavar="PATH", help="path where to create benchmark input data")
subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines.")
@ -434,14 +421,8 @@ class BenchmarkMixIn:
- enough free memory so there will be no slow down due to paging activity
"""
)
subparser = benchmark_parsers.add_parser(
"cpu",
parents=[common_parser],
add_help=False,
description=self.do_benchmark_cpu.__doc__,
epilog=bench_cpu_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="benchmarks Borg CPU-bound operations.",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_benchmark_cpu.__doc__, epilog=bench_cpu_epilog
)
subparser.set_defaults(func=self.do_benchmark_cpu)
benchmark_parsers.add_subcommand("cpu", subparser, help="benchmarks Borg CPU-bound operations.")
subparser.add_argument("--json", action="store_true", help="format output as JSON")

View file

@ -1,9 +1,9 @@
import argparse
from ._common import with_repository, Highlander
from ..archive import ArchiveChecker
from ..constants import * # NOQA
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError
from ..helpers import yes
from ..helpers.argparsing import ArgumentParser
from ..logger import create_logger
@ -60,7 +60,7 @@ class CheckMixIn:
repair=args.repair,
find_lost_archives=args.find_lost_archives,
match=args.match_archives,
sort_by=args.sort_by or "ts",
sort_by=args.sort_by or "timestamp",
first=args.first,
last=args.last,
older=args.older,
@ -182,16 +182,8 @@ class CheckMixIn:
``borg compact`` would remove the archives' data completely.
"""
)
subparser = subparsers.add_parser(
"check",
parents=[common_parser],
add_help=False,
description=self.do_check.__doc__,
epilog=check_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="verify the repository",
)
subparser.set_defaults(func=self.do_check)
subparser = ArgumentParser(parents=[common_parser], description=self.do_check.__doc__, epilog=check_epilog)
subparsers.add_subcommand("check", subparser, help="verify the repository")
subparser.add_argument(
"--repository-only", dest="repo_only", action="store_true", help="only perform repository checks"
)

View file

@ -1,4 +1,3 @@
import argparse
from pathlib import Path
from ._common import with_repository
@ -6,6 +5,7 @@ from ..archive import Archive
from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo
from ..cache import files_cache_name, discover_files_cache_names
from ..helpers import get_cache_dir
from ..helpers.argparsing import ArgumentParser
from ..constants import * # NOQA
from ..hashindex import ChunkIndex, ChunkIndexEntry
from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex
@ -257,16 +257,8 @@ class CompactMixIn:
thus it cannot compute before/after compaction size statistics).
"""
)
subparser = subparsers.add_parser(
"compact",
parents=[common_parser],
add_help=False,
description=self.do_compact.__doc__,
epilog=compact_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="compact the repository",
)
subparser.set_defaults(func=self.do_compact)
subparser = ArgumentParser(parents=[common_parser], description=self.do_compact.__doc__, epilog=compact_epilog)
subparsers.add_subcommand("compact", subparser, help="compact the repository")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
)

View file

@ -50,8 +50,6 @@ The following argument types have intelligent, context-aware completion:
- Suggests common file size values (500M, 1G, 10G, 100G, 1T, etc.)
"""
import argparse
import shtab
from ._common import process_epilog
@ -62,12 +60,14 @@ from ..helpers import (
FilesCacheMode,
PathSpec,
ChunkerParams,
CompressionSpec,
tag_validator,
relative_time_marker_validator,
parse_file_size,
)
from ..helpers.argparsing import ArgumentParser
from ..helpers.argparsing import ActionSubCommands
from ..helpers.time import timestamp
from ..compress import CompressionSpec
from ..helpers.parseformat import partial_format
from ..manifest import AI_HUMAN_SORT_KEYS
@ -341,7 +341,6 @@ _borg_help_topics() {
}
"""
# Global zsh preamble providing dynamic completion for aid:<hex> archive IDs.
#
# Notes:
@ -628,12 +627,11 @@ _borg_help_topics() {
"""
def _attach_completion(parser: argparse.ArgumentParser, type_class, completion_dict: dict):
def _attach_completion(parser: ArgumentParser, type_class, completion_dict: dict):
"""Tag all arguments with type `type_class` with completion choices from `completion_dict`."""
for action in parser._actions:
# Recurse into subparsers
if isinstance(action, argparse._SubParsersAction):
if isinstance(action, ActionSubCommands):
for sub in action.choices.values():
_attach_completion(sub, type_class, completion_dict)
continue
@ -642,10 +640,10 @@ def _attach_completion(parser: argparse.ArgumentParser, type_class, completion_d
action.complete = completion_dict # type: ignore[attr-defined]
def _attach_help_completion(parser: argparse.ArgumentParser, completion_dict: dict):
def _attach_help_completion(parser: ArgumentParser, completion_dict: dict):
"""Tag the 'topic' argument of the 'help' command with static completion choices."""
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
if isinstance(action, ActionSubCommands):
for sub in action.choices.values():
_attach_help_completion(sub, completion_dict)
continue
@ -692,7 +690,7 @@ class CompletionMixIn:
# Collect all commands and help topics for "borg help" completion
help_choices = list(self.helptext.keys())
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
if isinstance(action, ActionSubCommands):
help_choices.extend(action.choices.keys())
help_completion_fn = "_borg_help_topics"
@ -732,8 +730,14 @@ class CompletionMixIn:
}
bash_preamble = partial_format(BASH_PREAMBLE_TMPL, mapping)
zsh_preamble = partial_format(ZSH_PREAMBLE_TMPL, mapping)
preamble = {"bash": bash_preamble, "zsh": zsh_preamble}
script = shtab.complete(parser, shell=args.shell, preamble=preamble) # nosec B604
if args.shell == "bash":
preambles = [bash_preamble]
elif args.shell == "zsh":
preambles = [zsh_preamble]
else:
preambles = []
script = parser.get_completion_script(f"shtab-{args.shell}", preambles=preambles)
print(script)
def build_parser_completion(self, subparsers, common_parser, mid_common_parser):
@ -750,16 +754,10 @@ class CompletionMixIn:
"""
)
subparser = subparsers.add_parser(
"completion",
parents=[common_parser],
add_help=False,
description=self.do_completion.__doc__,
epilog=completion_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="output shell completion script",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_completion.__doc__, epilog=completion_epilog
)
subparser.set_defaults(func=self.do_completion)
subparsers.add_subcommand("completion", subparser, help="output shell completion script")
subparser.add_argument(
"shell", metavar="SHELL", choices=shells, help="shell to generate completion for (one of: %(choices)s)"
)

View file

@ -1,6 +1,5 @@
import errno
import sys
import argparse
import logging
import os
import posixpath
@ -16,9 +15,8 @@ from ..archive import BackupError, BackupOSError, BackupItemExcluded, backup_io,
from ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProcessor
from ..cache import Cache
from ..constants import * # NOQA
from ..compress import CompressionSpec
from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec
from ..helpers import archivename_validator, FilesCacheMode
from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec, CompressionSpec
from ..helpers import archivename_validator, FilesCacheMode, octal_int
from ..helpers import eval_escapes
from ..helpers import timestamp, archive_ts_now
from ..helpers import get_cache_dir, os_stat, get_strip_prefix, slashify
@ -31,6 +29,7 @@ from ..helpers import sig_int, ignore_sigint
from ..helpers import iter_separated
from ..helpers import MakePathSafeAction
from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..patterns import PatternMatcher
from ..platform import is_win32
@ -92,13 +91,24 @@ class CreateMixIn:
else:
status = "+" # included
self.print_file_status(status, path)
elif args.paths_from_command or args.paths_from_stdin:
elif args.paths_from_command or args.paths_from_shell_command or args.paths_from_stdin:
paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else "\n"
if args.paths_from_command:
if args.paths_from_command or args.paths_from_shell_command:
try:
env = prepare_subprocess_env(system=True)
proc = subprocess.Popen( # nosec B603
args.paths, stdout=subprocess.PIPE, env=env, preexec_fn=None if is_win32 else ignore_sigint
if args.paths_from_shell_command:
# Use shell=True to support pipes, redirection, etc.
shell = True
cmd = " ".join(args.paths)
else:
shell = False
cmd = args.paths
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
env=env,
shell=shell, # nosec B602
preexec_fn=None if is_win32 else ignore_sigint,
)
except (FileNotFoundError, PermissionError) as e:
raise CommandError(f"Failed to execute command: {e}")
@ -132,12 +142,13 @@ class CreateMixIn:
self.print_file_status(status, path)
if not dry_run and status is not None:
fso.stats.files_stats[status] += 1
if args.paths_from_command:
if args.paths_from_command or args.paths_from_shell_command:
rc = proc.wait()
if rc != 0:
raise CommandError(f"Command {args.paths[0]!r} exited with status {rc}")
else:
for path in args.paths:
paths = list(args.pattern_roots) + list(args.paths)
for path in paths:
if path == "": # issue #5637
self.print_warning("An empty string was given as PATH, ignoring.")
continue
@ -678,7 +689,6 @@ class CreateMixIn:
macOS examples are the apfs mounts of a typical macOS installation.
Therefore, when using ``--one-file-system``, you should double-check that the backup works as intended.
.. _list_item_flags:
Item flags
@ -763,24 +773,16 @@ class CreateMixIn:
If you need more control and you want to give every single fs object path
to borg (maybe implementing your own recursion or your own rules), you can use
``--paths-from-stdin`` or ``--paths-from-command`` (with the latter, borg will
fail to create an archive should the command fail).
``--paths-from-stdin``, ``--paths-from-command`` or ``--paths-from-shell-command``
(with the latter two, borg will fail to create an archive should the command fail).
Borg supports paths with the slashdot hack to strip path prefixes here also.
So, be careful not to unintentionally trigger that.
"""
)
subparser = subparsers.add_parser(
"create",
parents=[common_parser],
add_help=False,
description=self.do_create.__doc__,
epilog=create_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="create a backup",
)
subparser.set_defaults(func=self.do_create)
subparser = ArgumentParser(parents=[common_parser], description=self.do_create.__doc__, epilog=create_epilog)
subparsers.add_subcommand("create", subparser, help="create a backup")
# note: --dry-run and --stats are mutually exclusive, but we do not want to abort when
# parsing, but rather proceed with the dry-run, but without stats (see run() method).
@ -830,7 +832,7 @@ class CreateMixIn:
"--stdin-mode",
metavar="M",
dest="stdin_mode",
type=lambda s: int(s, 8),
type=octal_int,
default=STDIN_MODE_DEFAULT,
action=Highlander,
help="set mode to M in archive for stdin data (default: %(default)04o)",
@ -851,6 +853,11 @@ class CreateMixIn:
action="store_true",
help="interpret PATH as command and treat its output as ``--paths-from-stdin``",
)
subparser.add_argument(
"--paths-from-shell-command",
action="store_true",
help="interpret PATH as shell command and treat its output as ``--paths-from-stdin``",
)
subparser.add_argument(
"--paths-delimiter",
action=Highlander,

View file

@ -1,18 +1,16 @@
import argparse
import functools
import json
import textwrap
from ..archive import Archive
from ..compress import CompressionSpec
from ..constants import * # NOQA
from ..helpers import msgpack
from ..helpers import sysinfo
from ..helpers import bin_to_hex, hex_to_bin, prepare_dump_dict
from ..helpers import dash_open
from ..helpers import StableDict
from ..helpers import archivename_validator
from ..helpers import archivename_validator, CompressionSpec
from ..helpers import CommandError, RTError
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..platform import get_process_id
from ..repository import Repository, LIST_SCAN_LIMIT, repo_lister
@ -319,18 +317,14 @@ class DebugMixIn:
what you are doing or if a trusted developer tells you what to do."""
)
subparser = subparsers.add_parser(
"debug",
subparser = ArgumentParser(
parents=[mid_common_parser],
add_help=False,
description="debugging command (not intended for normal use)",
epilog=debug_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="debugging command (not intended for normal use)",
)
subparsers.add_subcommand("debug", subparser, help="debugging command (not intended for normal use)")
debug_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
debug_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
debug_info_epilog = process_epilog(
"""
@ -339,32 +333,22 @@ class DebugMixIn:
already appended at the end of the traceback.
"""
)
subparser = debug_parsers.add_parser(
"info",
parents=[common_parser],
add_help=False,
description=self.do_debug_info.__doc__,
epilog=debug_info_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="show system infos for debugging / bug reports (debug)",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_info.__doc__, epilog=debug_info_epilog
)
subparser.set_defaults(func=self.do_debug_info)
debug_parsers.add_subcommand("info", subparser, help="show system infos for debugging / bug reports (debug)")
debug_dump_archive_items_epilog = process_epilog(
"""
This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files.
"""
)
subparser = debug_parsers.add_parser(
"dump-archive-items",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_dump_archive_items.__doc__,
epilog=debug_dump_archive_items_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="dump archive items (metadata) (debug)",
)
subparser.set_defaults(func=self.do_debug_dump_archive_items)
debug_parsers.add_subcommand("dump-archive-items", subparser, help="dump archive items (metadata) (debug)")
subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
debug_dump_archive_epilog = process_epilog(
@ -372,16 +356,12 @@ class DebugMixIn:
This command dumps all metadata of an archive in a decoded form to a file.
"""
)
subparser = debug_parsers.add_parser(
"dump-archive",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_dump_archive.__doc__,
epilog=debug_dump_archive_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="dump decoded archive metadata (debug)",
)
subparser.set_defaults(func=self.do_debug_dump_archive)
debug_parsers.add_subcommand("dump-archive", subparser, help="dump decoded archive metadata (debug)")
subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into")
@ -390,16 +370,12 @@ class DebugMixIn:
This command dumps manifest metadata of a repository in a decoded form to a file.
"""
)
subparser = debug_parsers.add_parser(
"dump-manifest",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_dump_manifest.__doc__,
epilog=debug_dump_manifest_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="dump decoded repository metadata (debug)",
)
subparser.set_defaults(func=self.do_debug_dump_manifest)
debug_parsers.add_subcommand("dump-manifest", subparser, help="dump decoded repository metadata (debug)")
subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into")
debug_dump_repo_objs_epilog = process_epilog(
@ -407,32 +383,24 @@ class DebugMixIn:
This command dumps raw (but decrypted and decompressed) repo objects to files.
"""
)
subparser = debug_parsers.add_parser(
"dump-repo-objs",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_dump_repo_objs.__doc__,
epilog=debug_dump_repo_objs_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="dump repo objects (debug)",
)
subparser.set_defaults(func=self.do_debug_dump_repo_objs)
debug_parsers.add_subcommand("dump-repo-objs", subparser, help="dump repo objects (debug)")
debug_search_repo_objs_epilog = process_epilog(
"""
This command searches raw (but decrypted and decompressed) repo objects for a specific bytes sequence.
"""
)
subparser = debug_parsers.add_parser(
"search-repo-objs",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_search_repo_objs.__doc__,
epilog=debug_search_repo_objs_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="search repo objects (debug)",
)
subparser.set_defaults(func=self.do_debug_search_repo_objs)
debug_parsers.add_subcommand("search-repo-objs", subparser, help="search repo objects (debug)")
subparser.add_argument(
"wanted",
metavar="WANTED",
@ -445,16 +413,10 @@ class DebugMixIn:
This command computes the id-hash for some file content.
"""
)
subparser = debug_parsers.add_parser(
"id-hash",
parents=[common_parser],
add_help=False,
description=self.do_debug_id_hash.__doc__,
epilog=debug_id_hash_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="compute id-hash for some file content (debug)",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_id_hash.__doc__, epilog=debug_id_hash_epilog
)
subparser.set_defaults(func=self.do_debug_id_hash)
debug_parsers.add_subcommand("id-hash", subparser, help="compute id-hash for some file content (debug)")
subparser.add_argument(
"path", metavar="PATH", type=str, help="content for which the id-hash shall get computed"
)
@ -465,16 +427,10 @@ class DebugMixIn:
This command parses the object file into metadata (as json) and uncompressed data.
"""
)
subparser = debug_parsers.add_parser(
"parse-obj",
parents=[common_parser],
add_help=False,
description=self.do_debug_parse_obj.__doc__,
epilog=debug_parse_obj_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="parse borg object file into meta dict and data",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_parse_obj.__doc__, epilog=debug_parse_obj_epilog
)
subparser.set_defaults(func=self.do_debug_parse_obj)
debug_parsers.add_subcommand("parse-obj", subparser, help="parse borg object file into meta dict and data")
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
subparser.add_argument(
"object_path", metavar="OBJECT_PATH", type=str, help="path of the object file to parse data from"
@ -492,16 +448,10 @@ class DebugMixIn:
This command formats the file and metadata into a Borg object file.
"""
)
subparser = debug_parsers.add_parser(
"format-obj",
parents=[common_parser],
add_help=False,
description=self.do_debug_format_obj.__doc__,
epilog=debug_format_obj_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="format file and metadata into a Borg object file",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_format_obj.__doc__, epilog=debug_format_obj_epilog
)
subparser.set_defaults(func=self.do_debug_format_obj)
debug_parsers.add_subcommand("format-obj", subparser, help="format file and metadata into a Borg object file")
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
subparser.add_argument(
"binary_path", metavar="BINARY_PATH", type=str, help="path of the file to convert into an object file"
@ -531,16 +481,10 @@ class DebugMixIn:
This command gets an object from the repository.
"""
)
subparser = debug_parsers.add_parser(
"get-obj",
parents=[common_parser],
add_help=False,
description=self.do_debug_get_obj.__doc__,
epilog=debug_get_obj_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="get object from repository (debug)",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog
)
subparser.set_defaults(func=self.do_debug_get_obj)
debug_parsers.add_subcommand("get-obj", subparser, help="get object from repository (debug)")
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
subparser.add_argument("path", metavar="PATH", type=str, help="file to write object data into")
@ -549,16 +493,10 @@ class DebugMixIn:
This command puts an object into the repository.
"""
)
subparser = debug_parsers.add_parser(
"put-obj",
parents=[common_parser],
add_help=False,
description=self.do_debug_put_obj.__doc__,
epilog=debug_put_obj_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="put object to repository (debug)",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog
)
subparser.set_defaults(func=self.do_debug_put_obj)
debug_parsers.add_subcommand("put-obj", subparser, help="put object to repository (debug)")
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to put into the repo")
subparser.add_argument("path", metavar="PATH", type=str, help="file to read and create object from")
@ -567,16 +505,10 @@ class DebugMixIn:
This command deletes objects from the repository.
"""
)
subparser = debug_parsers.add_parser(
"delete-obj",
parents=[common_parser],
add_help=False,
description=self.do_debug_delete_obj.__doc__,
epilog=debug_delete_obj_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="delete object from repository (debug)",
subparser = ArgumentParser(
parents=[mid_common_parser], description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog
)
subparser.set_defaults(func=self.do_debug_delete_obj)
debug_parsers.add_subcommand("delete-obj", subparser, help="delete object from repository (debug)")
subparser.add_argument(
"ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to delete from the repo"
)
@ -586,15 +518,13 @@ class DebugMixIn:
Convert a Borg profile to a Python cProfile compatible profile.
"""
)
subparser = debug_parsers.add_parser(
"convert-profile",
parents=[common_parser],
add_help=False,
subparser = ArgumentParser(
parents=[mid_common_parser],
description=self.do_debug_convert_profile.__doc__,
epilog=debug_convert_profile_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="convert Borg profile to Python profile (debug)",
)
subparser.set_defaults(func=self.do_debug_convert_profile)
debug_parsers.add_subcommand(
"convert-profile", subparser, help="convert Borg profile to Python profile (debug)"
)
subparser.add_argument("input", metavar="INPUT", type=str, help="Borg profile")
subparser.add_argument("output", metavar="OUTPUT", type=str, help="Output file")

View file

@ -1,9 +1,9 @@
import argparse
import logging
from ._common import with_repository
from ..constants import * # NOQA
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -80,16 +80,8 @@ class DeleteMixIn:
patterns, see :ref:`borg_patterns`).
"""
)
subparser = subparsers.add_parser(
"delete",
parents=[common_parser],
add_help=False,
description=self.do_delete.__doc__,
epilog=delete_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="delete archives",
)
subparser.set_defaults(func=self.do_delete)
subparser = ArgumentParser(parents=[common_parser], description=self.do_delete.__doc__, epilog=delete_epilog)
subparsers.add_subcommand("delete", subparser, help="delete archives")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
)

View file

@ -1,4 +1,3 @@
import argparse
import textwrap
import json
import sys
@ -9,6 +8,7 @@ from ..archive import Archive
from ..constants import * # NOQA
from ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder
from ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates
from ..helpers.argparsing import ArgumentParser, ArgumentTypeError
from ..item import ItemDiff
from ..manifest import Manifest
from ..logger import create_logger
@ -84,6 +84,7 @@ class DiffMixIn:
wc=None,
)
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(args.patterns, args.paths)
diffs_iter = Archive.compare_archives_iter(
@ -203,7 +204,6 @@ class DiffMixIn:
The following keys are always available:
"""
)
+ BaseFormatter.keys_help()
@ -268,7 +268,7 @@ class DiffMixIn:
def diff_sort_spec_validator(s):
if not isinstance(s, str):
raise argparse.ArgumentTypeError("unsupported sort field (not a string)")
raise ArgumentTypeError("unsupported sort field (not a string)")
allowed = {
"path",
"size_added",
@ -286,23 +286,15 @@ class DiffMixIn:
}
parts = [p.strip() for p in s.split(",") if p.strip()]
if not parts:
raise argparse.ArgumentTypeError("unsupported sort field: empty spec")
raise ArgumentTypeError("unsupported sort field: empty spec")
for spec in parts:
field = spec[1:] if spec and spec[0] in (">", "<") else spec
if field not in allowed:
raise argparse.ArgumentTypeError(f"unsupported sort field: {field}")
raise ArgumentTypeError(f"unsupported sort field: {field}")
return ",".join(parts)
subparser = subparsers.add_parser(
"diff",
parents=[common_parser],
add_help=False,
description=self.do_diff.__doc__,
epilog=diff_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="find differences in archive contents",
)
subparser.set_defaults(func=self.do_diff)
subparser = ArgumentParser(parents=[common_parser], description=self.do_diff.__doc__, epilog=diff_epilog)
subparsers.add_subcommand("diff", subparser, help="find differences in archive contents")
subparser.add_argument(
"--numeric-ids",
dest="numeric_ids",

View file

@ -1,5 +1,4 @@
import sys
import argparse
import logging
import stat
@ -12,6 +11,7 @@ from ..helpers import remove_surrogates
from ..helpers import HardLinkManager
from ..helpers import ProgressIndicatorPercent
from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -33,6 +33,7 @@ class ExtractMixIn:
"For example, install locales and use: LANG=en_US.UTF-8"
)
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(args.patterns, args.paths)
progress = args.progress
@ -154,16 +155,8 @@ class ExtractMixIn:
group, permissions, etc.
"""
)
subparser = subparsers.add_parser(
"extract",
parents=[common_parser],
add_help=False,
description=self.do_extract.__doc__,
epilog=extract_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="extract archive contents",
)
subparser.set_defaults(func=self.do_extract)
subparser = ArgumentParser(parents=[common_parser], description=self.do_extract.__doc__, epilog=extract_epilog)
subparsers.add_subcommand("extract", subparser, help="extract archive contents")
subparser.add_argument(
"--list", dest="output_list", action="store_true", help="output a verbose list of items (files, dirs, ...)"
)

View file

@ -1,7 +1,7 @@
import collections
import functools
import textwrap
from ..helpers.argparsing import ArgumentParser
from ..constants import * # NOQA
from ..helpers.nanorst import rst_to_terminal
@ -161,7 +161,6 @@ class HelpMixIn:
# not '/home/user/importantjunk' or '/etc/junk':
$ borg create -e 'home/*/junk' archive /
# The contents of directories in '/home' are not backed up when their name
# ends in '.tmp'
$ borg create --exclude 're:^home/[^/]+\\.tmp/' archive /
@ -523,7 +522,10 @@ class HelpMixIn:
borg create --compression obfuscate,250,zstd,3 ...\n\n"""
)
def do_help(self, parser, commands, args):
def do_help(self, parser, args):
commands = getattr(parser, "_subcommands_action", None)
commands = commands._name_parser_map if commands else {}
if not args.topic:
parser.print_help()
elif args.topic in self.helptext:
@ -551,10 +553,8 @@ class HelpMixIn:
do_maincommand_help = do_subcommand_help
def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser):
subparser = subparsers.add_parser(
"help", parents=[common_parser], add_help=False, description="Extra help", help="Extra help"
)
subparser = ArgumentParser(parents=[common_parser], description="Extra help")
subparsers.add_subcommand("help", subparser, help="Extra help")
subparser.add_argument("--epilog-only", dest="epilog_only", action="store_true")
subparser.add_argument("--usage-only", dest="usage_only", action="store_true")
subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices))
subparser.add_argument("topic", metavar="TOPIC", type=str, nargs="?", help="additional help on TOPIC")

View file

@ -1,4 +1,3 @@
import argparse
import textwrap
from datetime import timedelta
@ -6,6 +5,7 @@ from ._common import with_repository
from ..archive import Archive
from ..constants import * # NOQA
from ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -78,16 +78,8 @@ class InfoMixIn:
= all chunks in the repository.
"""
)
subparser = subparsers.add_parser(
"info",
parents=[common_parser],
add_help=False,
description=self.do_info.__doc__,
epilog=info_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="show repository or archive information",
)
subparser.set_defaults(func=self.do_info)
subparser = ArgumentParser(parents=[common_parser], description=self.do_info.__doc__, epilog=info_epilog)
subparsers.add_subcommand("info", subparser, help="show repository or archive information")
subparser.add_argument("--json", action="store_true", help="format output as JSON")
define_archive_filters_group(subparser)
subparser.add_argument(

View file

@ -1,5 +1,3 @@
import argparse
import functools
import os
from ..constants import * # NOQA
@ -7,6 +5,7 @@ from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2
from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
from ..crypto.keymanager import KeyManager
from ..helpers import PathSpec, CommandError
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ._common import with_repository
@ -18,7 +17,7 @@ logger = create_logger(__name__)
class KeysMixIn:
@with_repository(compatibility=(Manifest.Operation.CHECK,))
def do_change_passphrase(self, args, repository, manifest):
def do_key_change_passphrase(self, args, repository, manifest):
"""Changes the repository key file passphrase."""
key = manifest.key
if not hasattr(key, "change_passphrase"):
@ -30,7 +29,7 @@ class KeysMixIn:
logger.info("Key location: %s", key.find_key())
@with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))
def do_change_location(self, args, repository, manifest, cache):
def do_key_change_location(self, args, repository, manifest, cache):
"""Changes the repository key location."""
key = manifest.key
if not hasattr(key, "change_passphrase"):
@ -120,18 +119,12 @@ class KeysMixIn:
def build_parser_keys(self, subparsers, common_parser, mid_common_parser):
from ._common import process_epilog
subparser = subparsers.add_parser(
"key",
parents=[mid_common_parser],
add_help=False,
description="Manage the keyfile or repokey of a repository",
epilog="",
formatter_class=argparse.RawDescriptionHelpFormatter,
help="manage the repository key",
subparser = ArgumentParser(
parents=[mid_common_parser], description="Manage the keyfile or repokey of a repository", epilog=""
)
subparsers.add_subcommand("key", subparser, help="manage the repository key")
key_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
key_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
key_export_epilog = process_epilog(
"""
@ -164,16 +157,10 @@ class KeysMixIn:
HTML template with a QR code and a copy of the ``--paper``-formatted key.
"""
)
subparser = key_parsers.add_parser(
"export",
parents=[common_parser],
add_help=False,
description=self.do_key_export.__doc__,
epilog=key_export_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="export the repository key for backup",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_key_export.__doc__, epilog=key_export_epilog
)
subparser.set_defaults(func=self.do_key_export)
key_parsers.add_subcommand("export", subparser, help="export the repository key for backup")
subparser.add_argument("path", metavar="PATH", nargs="?", type=PathSpec, help="where to store the backup")
subparser.add_argument(
"--paper",
@ -206,16 +193,10 @@ class KeysMixIn:
key import`` creates a new key file in ``$BORG_KEYS_DIR``.
"""
)
subparser = key_parsers.add_parser(
"import",
parents=[common_parser],
add_help=False,
description=self.do_key_import.__doc__,
epilog=key_import_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="import the repository key from backup",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_key_import.__doc__, epilog=key_import_epilog
)
subparser.set_defaults(func=self.do_key_import)
key_parsers.add_subcommand("import", subparser, help="import the repository key from backup")
subparser.add_argument(
"path", metavar="PATH", nargs="?", type=PathSpec, help="path to the backup ('-' to read from stdin)"
)
@ -237,16 +218,10 @@ class KeysMixIn:
does not protect future (nor past) backups to the same repository.
"""
)
subparser = key_parsers.add_parser(
"change-passphrase",
parents=[common_parser],
add_help=False,
description=self.do_change_passphrase.__doc__,
epilog=change_passphrase_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="change the repository passphrase",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog
)
subparser.set_defaults(func=self.do_change_passphrase)
key_parsers.add_subcommand("change-passphrase", subparser, help="change the repository passphrase")
change_location_epilog = process_epilog(
"""
@ -261,16 +236,10 @@ class KeysMixIn:
thus you must ONLY give the key location (keyfile or repokey).
"""
)
subparser = key_parsers.add_parser(
"change-location",
parents=[common_parser],
add_help=False,
description=self.do_change_location.__doc__,
epilog=change_location_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="change the key location",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_key_change_location.__doc__, epilog=change_location_epilog
)
subparser.set_defaults(func=self.do_change_location)
key_parsers.add_subcommand("change-location", subparser, help="change the key location")
subparser.add_argument(
"key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location"
)

View file

@ -1,4 +1,3 @@
import argparse
import os
import textwrap
import sys
@ -8,6 +7,7 @@ from ..archive import Archive
from ..cache import Cache
from ..constants import * # NOQA
from ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -19,6 +19,7 @@ class ListMixIn:
@with_repository(compatibility=(Manifest.Operation.READ,))
def do_list(self, args, repository, manifest):
"""List archive contents."""
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(args.patterns, args.paths)
if args.format is not None:
format = args.format
@ -89,7 +90,6 @@ class ListMixIn:
The following keys are always available:
"""
)
+ BaseFormatter.keys_help()
@ -102,16 +102,8 @@ class ListMixIn:
)
+ ItemFormatter.keys_help()
)
subparser = subparsers.add_parser(
"list",
parents=[common_parser],
add_help=False,
description=self.do_list.__doc__,
epilog=list_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="list archive contents",
)
subparser.set_defaults(func=self.do_list)
subparser = ArgumentParser(parents=[common_parser], description=self.do_list.__doc__, epilog=list_epilog)
subparsers.add_subcommand("list", subparser, help="list archive contents")
subparser.add_argument(
"--short", dest="short", action="store_true", help="only print file/directory names, nothing else"
)

View file

@ -1,10 +1,10 @@
import argparse
import subprocess
from ._common import with_repository
from ..cache import Cache
from ..constants import * # NOQA
from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner
from ..helpers.argparsing import ArgumentParser, REMAINDER
from ..logger import create_logger
@ -45,16 +45,10 @@ class LocksMixIn:
trying to access the cache or the repository.
"""
)
subparser = subparsers.add_parser(
"break-lock",
parents=[common_parser],
add_help=False,
description=self.do_break_lock.__doc__,
epilog=break_lock_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="break the repository and cache locks",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_break_lock.__doc__, epilog=break_lock_epilog
)
subparser.set_defaults(func=self.do_break_lock)
subparsers.add_subcommand("break-lock", subparser, help="break the repository and cache locks")
with_lock_epilog = process_epilog(
"""
@ -77,15 +71,9 @@ class LocksMixIn:
Borg is cautious and does not automatically remove stale locks made by a different host.
"""
)
subparser = subparsers.add_parser(
"with-lock",
parents=[common_parser],
add_help=False,
description=self.do_with_lock.__doc__,
epilog=with_lock_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="run a user command with the lock held",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_with_lock.__doc__, epilog=with_lock_epilog
)
subparser.set_defaults(func=self.do_with_lock)
subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held")
subparser.add_argument("command", metavar="COMMAND", help="command to run")
subparser.add_argument("args", metavar="ARGS", nargs=argparse.REMAINDER, help="command arguments")
subparser.add_argument("args", metavar="ARGS", nargs=REMAINDER, help="command arguments")

View file

@ -1,4 +1,3 @@
import argparse
import os
from ._common import with_repository, Highlander
@ -6,6 +5,7 @@ from ..constants import * # NOQA
from ..helpers import RTError
from ..helpers import PathSpec
from ..helpers import umount
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..remote import cache_if_remote
@ -151,15 +151,8 @@ class MountMixIn:
the logger to output to a file.
"""
)
subparser = subparsers.add_parser(
"mount",
parents=[common_parser],
add_help=False,
description=self.do_mount.__doc__,
epilog=mount_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="mount a repository",
)
subparser = ArgumentParser(parents=[common_parser], description=self.do_mount.__doc__, epilog=mount_epilog)
subparsers.add_subcommand("mount", subparser, help="mount a repository")
self._define_borg_mount(subparser)
umount_epilog = process_epilog(
@ -170,16 +163,8 @@ class MountMixIn:
command - usually this is either umount or fusermount -u.
"""
)
subparser = subparsers.add_parser(
"umount",
parents=[common_parser],
add_help=False,
description=self.do_umount.__doc__,
epilog=umount_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="unmount a repository",
)
subparser.set_defaults(func=self.do_umount)
subparser = ArgumentParser(parents=[common_parser], description=self.do_umount.__doc__, epilog=umount_epilog)
subparsers.add_subcommand("umount", subparser, help="unmount a repository")
subparser.add_argument(
"mountpoint", metavar="MOUNTPOINT", type=str, help="mountpoint of the filesystem to unmount"
)
@ -188,7 +173,6 @@ class MountMixIn:
assert parser.prog == "borgfs"
parser.description = self.do_mount.__doc__
parser.epilog = "For more information, see borg mount --help."
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.help = "mount a repository"
self._define_borg_mount(parser)
return parser
@ -196,7 +180,6 @@ class MountMixIn:
def _define_borg_mount(self, parser):
from ._common import define_exclusion_group, define_archive_filters_group
parser.set_defaults(func=self.do_mount)
parser.add_argument("mountpoint", metavar="MOUNTPOINT", type=str, help="where to mount the filesystem")
parser.add_argument(
"-f", "--foreground", dest="foreground", action="store_true", help="stay in foreground, do not daemonize"

View file

@ -1,4 +1,3 @@
import argparse
from collections import OrderedDict
from datetime import datetime, timezone, timedelta
import logging
@ -9,6 +8,7 @@ from ._common import with_repository, Highlander
from ..constants import * # NOQA
from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error
from ..helpers import archivename_validator
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -273,16 +273,8 @@ class PruneMixIn:
the ``borg repo-list`` description for more details about the format string).
"""
)
subparser = subparsers.add_parser(
"prune",
parents=[common_parser],
add_help=False,
description=self.do_prune.__doc__,
epilog=prune_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="prune archives",
)
subparser.set_defaults(func=self.do_prune)
subparser = ArgumentParser(parents=[common_parser], description=self.do_prune.__doc__, epilog=prune_epilog)
subparsers.add_subcommand("prune", subparser, help="prune archives")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
)

View file

@ -1,12 +1,10 @@
import argparse
from ._common import with_repository, Highlander
from ._common import build_matcher
from ..archive import ArchiveRecreater
from ..constants import * # NOQA
from ..compress import CompressionSpec
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex, CompressionSpec
from ..helpers import timestamp
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -18,6 +16,7 @@ class RecreateMixIn:
@with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,))
def do_recreate(self, args, repository, manifest, cache):
"""Recreate archives."""
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(args.patterns, args.paths)
self.output_list = args.output_list
self.output_filter = args.output_filter
@ -101,16 +100,10 @@ class RecreateMixIn:
if the chunks are still missing.
"""
)
subparser = subparsers.add_parser(
"recreate",
parents=[common_parser],
add_help=False,
description=self.do_recreate.__doc__,
epilog=recreate_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help=self.do_recreate.__doc__,
subparser = ArgumentParser(
parents=[common_parser], description=self.do_recreate.__doc__, epilog=recreate_epilog
)
subparser.set_defaults(func=self.do_recreate)
subparsers.add_subcommand("recreate", subparser, help=self.do_recreate.__doc__)
subparser.add_argument(
"--list", dest="output_list", action="store_true", help="output verbose list of items (files, dirs, ...)"
)

View file

@ -1,8 +1,7 @@
import argparse
from ._common import with_repository, with_archive
from ..constants import * # NOQA
from ..helpers import archivename_validator
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -28,16 +27,8 @@ class RenameMixIn:
This results in a different archive ID.
"""
)
subparser = subparsers.add_parser(
"rename",
parents=[common_parser],
add_help=False,
description=self.do_rename.__doc__,
epilog=rename_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="rename an archive",
)
subparser.set_defaults(func=self.do_rename)
subparser = ArgumentParser(parents=[common_parser], description=self.do_rename.__doc__, epilog=rename_epilog)
subparsers.add_subcommand("rename", subparser, help="rename an archive")
subparser.add_argument(
"name", metavar="OLDNAME", type=archivename_validator, help="specify the current archive name"
)

View file

@ -1,11 +1,11 @@
import argparse
from collections import defaultdict
from ._common import with_repository, Highlander
from ..constants import * # NOQA
from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE
from ..compress import ObfuscateSize, Auto, COMPRESSOR_TABLE
from ..hashindex import ChunkIndex
from ..helpers import sig_int, ProgressIndicatorPercent, Error
from ..helpers import sig_int, ProgressIndicatorPercent, Error, CompressionSpec
from ..helpers.argparsing import ArgumentParser
from ..repository import Repository
from ..remote import RemoteRepository
from ..manifest import Manifest
@ -180,16 +180,10 @@ class RepoCompressMixIn:
You do **not** need to run ``borg compact`` after ``borg repo-compress``.
"""
)
subparser = subparsers.add_parser(
"repo-compress",
parents=[common_parser],
add_help=False,
description=self.do_repo_compress.__doc__,
epilog=repo_compress_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help=self.do_repo_compress.__doc__,
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_compress.__doc__, epilog=repo_compress_epilog
)
subparser.set_defaults(func=self.do_repo_compress)
subparsers.add_subcommand("repo-compress", subparser, help=self.do_repo_compress.__doc__)
subparser.add_argument(
"-C",

View file

@ -1,11 +1,10 @@
import argparse
from ._common import with_repository, with_other_repository, Highlander
from ..cache import Cache
from ..constants import * # NOQA
from ..crypto.key import key_creator, key_argument_names
from ..helpers import CancelledByUser
from ..helpers import location_validator, Location
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -190,16 +189,10 @@ class RepoCreateMixIn:
Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives.
"""
)
subparser = subparsers.add_parser(
"repo-create",
parents=[common_parser],
add_help=False,
description=self.do_repo_create.__doc__,
epilog=repo_create_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="create a new, empty repository",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_create.__doc__, epilog=repo_create_epilog
)
subparser.set_defaults(func=self.do_repo_create)
subparsers.add_subcommand("repo-create", subparser, help="create a new, empty repository")
subparser.add_argument(
"--other-repo",
metavar="SRC_REPOSITORY",

View file

@ -1,5 +1,3 @@
import argparse
from ._common import with_repository
from ..cache import Cache, SecurityManager
from ..constants import * # NOQA
@ -7,6 +5,7 @@ from ..helpers import CancelledByUser
from ..helpers import format_archive
from ..helpers import bin_to_hex
from ..helpers import yes
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest, NoManifestError
from ..logger import create_logger
@ -102,16 +101,10 @@ class RepoDeleteMixIn:
Always first use ``--dry-run --list`` to see what would be deleted.
"""
)
subparser = subparsers.add_parser(
"repo-delete",
parents=[common_parser],
add_help=False,
description=self.do_repo_delete.__doc__,
epilog=repo_delete_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="delete a repository",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_delete.__doc__, epilog=repo_delete_epilog
)
subparser.set_defaults(func=self.do_repo_delete)
subparsers.add_subcommand("repo-delete", subparser, help="delete a repository")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
)

View file

@ -1,9 +1,9 @@
import argparse
import textwrap
from ._common import with_repository
from ..constants import * # NOQA
from ..helpers import bin_to_hex, json_print, basic_json_data
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -63,14 +63,8 @@ class RepoInfoMixIn:
This command displays detailed information about the repository.
"""
)
subparser = subparsers.add_parser(
"repo-info",
parents=[common_parser],
add_help=False,
description=self.do_repo_info.__doc__,
epilog=repo_info_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="show repository information",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_info.__doc__, epilog=repo_info_epilog
)
subparser.set_defaults(func=self.do_repo_info)
subparsers.add_subcommand("repo-info", subparser, help="show repository information")
subparser.add_argument("--json", action="store_true", help="format output as JSON")

View file

@ -1,4 +1,3 @@
import argparse
import os
import textwrap
import sys
@ -6,6 +5,7 @@ import sys
from ._common import with_repository, Highlander
from ..constants import * # NOQA
from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -72,7 +72,6 @@ class RepoListMixIn:
The following keys are always available:
"""
)
+ BaseFormatter.keys_help()
@ -85,16 +84,10 @@ class RepoListMixIn:
)
+ ArchiveFormatter.keys_help()
)
subparser = subparsers.add_parser(
"repo-list",
parents=[common_parser],
add_help=False,
description=self.do_repo_list.__doc__,
epilog=repo_list_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="list repository contents",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_list.__doc__, epilog=repo_list_epilog
)
subparser.set_defaults(func=self.do_repo_list)
subparsers.add_subcommand("repo-list", subparser, help="list repository contents")
subparser.add_argument(
"--short", dest="short", action="store_true", help="only print the archive IDs, nothing else"
)

View file

@ -1,4 +1,3 @@
import argparse
import math
import os
@ -7,6 +6,7 @@ from borgstore.store import ItemInfo
from ._common import with_repository, Highlander
from ..constants import * # NOQA
from ..helpers import parse_file_size, format_file_size
from ..helpers.argparsing import ArgumentParser
from ..logger import create_logger
@ -82,20 +82,13 @@ class RepoSpaceMixIn:
$ borg compact -v # only this actually frees space of deleted archives
$ borg repo-space --reserve 1G # reserve space again for next time
Reserved space is always rounded up to full reservation blocks of 64 MiB.
"""
)
subparser = subparsers.add_parser(
"repo-space",
parents=[common_parser],
add_help=False,
description=self.do_repo_space.__doc__,
epilog=repo_space_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="manage reserved space in a repository",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_repo_space.__doc__, epilog=repo_space_epilog
)
subparser.set_defaults(func=self.do_repo_space)
subparsers.add_subcommand("repo-space", subparser, help="manage reserved space in a repository")
subparser.add_argument(
"--reserve",
metavar="SPACE",

View file

@ -1,9 +1,8 @@
import argparse
from ..constants import * # NOQA
from ..remote import RepositoryServer
from ..logger import create_logger
from ..helpers.argparsing import ArgumentParser
logger = create_logger()
@ -52,16 +51,8 @@ class ServeMixIn:
Existing archives can be read, but no archives can be created or deleted.
"""
)
subparser = subparsers.add_parser(
"serve",
parents=[common_parser],
add_help=False,
description=self.do_serve.__doc__,
epilog=serve_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="start the repository server process",
)
subparser.set_defaults(func=self.do_serve)
subparser = ArgumentParser(parents=[common_parser], description=self.do_serve.__doc__, epilog=serve_epilog)
subparsers.add_subcommand("serve", subparser, help="start the repository server process")
subparser.add_argument(
"--restrict-to-path",
metavar="PATH",

View file

@ -1,9 +1,8 @@
import argparse
from ._common import with_repository, define_archive_filters_group
from ..archive import Archive
from ..constants import * # NOQA
from ..helpers import bin_to_hex, archivename_validator, tag_validator, Error
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -80,39 +79,12 @@ class TagMixIn:
removed).
"""
)
subparser = subparsers.add_parser(
"tag",
parents=[common_parser],
add_help=False,
description=self.do_tag.__doc__,
epilog=tag_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="tag archives",
)
subparser.set_defaults(func=self.do_tag)
subparser = ArgumentParser(parents=[common_parser], description=self.do_tag.__doc__, epilog=tag_epilog)
subparsers.add_subcommand("tag", subparser, help="tag archives")
subparser.add_argument("--set", dest="set_tags", metavar="TAG", type=tag_validator, nargs="+", help="set tags")
subparser.add_argument("--add", dest="add_tags", metavar="TAG", type=tag_validator, nargs="+", help="add tags")
subparser.add_argument(
"--set",
dest="set_tags",
metavar="TAG",
type=tag_validator,
action="append",
help="set tags (can be given multiple times)",
)
subparser.add_argument(
"--add",
dest="add_tags",
metavar="TAG",
type=tag_validator,
action="append",
help="add tags (can be given multiple times)",
)
subparser.add_argument(
"--remove",
dest="remove_tags",
metavar="TAG",
type=tag_validator,
action="append",
help="remove tags (can be given multiple times)",
"--remove", dest="remove_tags", metavar="TAG", type=tag_validator, nargs="+", help="remove tags"
)
define_archive_filters_group(subparser)
subparser.add_argument(

View file

@ -1,4 +1,3 @@
import argparse
import base64
import logging
import os
@ -6,7 +5,6 @@ import stat
import tarfile
from ..archive import Archive, TarfileObjectProcessors, ChunksProcessor
from ..compress import CompressionSpec
from ..constants import * # NOQA
from ..helpers import HardLinkManager, IncludePatternNeverMatchedWarning
from ..helpers import ProgressIndicatorPercent
@ -14,11 +12,12 @@ from ..helpers import dash_open
from ..helpers import msgpack
from ..helpers import create_filter_process
from ..helpers import ChunkIteratorFileWrapper
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, CompressionSpec
from ..helpers import remove_surrogates
from ..helpers import timestamp, archive_ts_now
from ..helpers import basic_json_data, json_print
from ..helpers import log_multi
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ._common import with_repository, with_archive, Highlander, define_exclusion_group
@ -86,6 +85,7 @@ class TarMixIn:
self._export_tar(args, archive, _stream)
def _export_tar(self, args, archive, tarstream):
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(args.patterns, args.paths)
progress = args.progress
@ -383,16 +383,10 @@ class TarMixIn:
pass over the archive metadata.
"""
)
subparser = subparsers.add_parser(
"export-tar",
parents=[common_parser],
add_help=False,
description=self.do_export_tar.__doc__,
epilog=export_tar_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="create tarball from archive",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_export_tar.__doc__, epilog=export_tar_epilog
)
subparser.set_defaults(func=self.do_export_tar)
subparsers.add_subcommand("export-tar", subparser, help="create tarball from archive")
subparser.add_argument(
"--tar-filter",
dest="tar_filter",
@ -459,16 +453,10 @@ class TarMixIn:
``--ignore-zeros`` option to skip through the stop markers between them.
"""
)
subparser = subparsers.add_parser(
"import-tar",
parents=[common_parser],
add_help=False,
description=self.do_import_tar.__doc__,
epilog=import_tar_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help=self.do_import_tar.__doc__,
subparser = ArgumentParser(
parents=[common_parser], description=self.do_import_tar.__doc__, epilog=import_tar_epilog
)
subparser.set_defaults(func=self.do_import_tar)
subparsers.add_subcommand("import-tar", subparser, help=self.do_import_tar.__doc__)
subparser.add_argument(
"--tar-filter",
dest="tar_filter",

View file

@ -1,15 +1,13 @@
import argparse
from ._common import with_repository, with_other_repository, Highlander
from ..archive import Archive, cached_hash, DownloadPipeline
from ..chunkers import get_chunker
from ..compress import CompressionSpec
from ..constants import * # NOQA
from ..crypto.key import uses_same_id_hash, uses_same_chunker_secret
from ..helpers import Error
from ..helpers import location_validator, Location, archivename_validator, comment_validator
from ..helpers import format_file_size, bin_to_hex
from ..helpers import ChunkerParams, ChunkIteratorFileWrapper
from ..helpers import ChunkerParams, ChunkIteratorFileWrapper, CompressionSpec
from ..helpers.argparsing import ArgumentParser, ArgumentTypeError
from ..item import ChunkListEntry
from ..manifest import Manifest
from ..legacyrepository import LegacyRepository
@ -156,7 +154,7 @@ class TransferMixIn:
for archive_info in archive_infos:
try:
archivename_validator(archive_info.name)
except argparse.ArgumentTypeError as err:
except ArgumentTypeError as err:
an_errors.append(str(err))
if an_errors:
an_errors.insert(0, "Invalid archive names detected, please rename them before transfer:")
@ -167,7 +165,7 @@ class TransferMixIn:
archive = Archive(other_manifest, archive_info.id)
try:
comment_validator(archive.metadata.get("comment", ""))
except argparse.ArgumentTypeError as err:
except ArgumentTypeError as err:
ac_errors.append(f"{archive_info.name}: {err}")
if ac_errors:
ac_errors.insert(0, "Invalid archive comments detected, please fix them before transfer:")
@ -309,7 +307,6 @@ class TransferMixIn:
borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it!
borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left?
Data migration / upgrade from borg 1.x
++++++++++++++++++++++++++++++++++++++
@ -330,19 +327,12 @@ class TransferMixIn:
borg --repo=DST_REPO transfer --other-repo=SRC_REPO \\
--chunker-params=buzhash,19,23,21,4095
"""
)
subparser = subparsers.add_parser(
"transfer",
parents=[common_parser],
add_help=False,
description=self.do_transfer.__doc__,
epilog=transfer_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="transfer of archives from another repository",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_transfer.__doc__, epilog=transfer_epilog
)
subparser.set_defaults(func=self.do_transfer)
subparsers.add_subcommand("transfer", subparser, help="transfer of archives from another repository")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change repository, just check"
)

View file

@ -1,9 +1,9 @@
import argparse
import logging
from ._common import with_repository
from ..constants import * # NOQA
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
from ..logger import create_logger
@ -72,16 +72,10 @@ class UnDeleteMixIn:
patterns, see :ref:`borg_patterns`).
"""
)
subparser = subparsers.add_parser(
"undelete",
parents=[common_parser],
add_help=False,
description=self.do_undelete.__doc__,
epilog=undelete_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="undelete archives",
subparser = ArgumentParser(
parents=[common_parser], description=self.do_undelete.__doc__, epilog=undelete_epilog
)
subparser.set_defaults(func=self.do_undelete)
subparsers.add_subcommand("undelete", subparser, help="undelete archives")
subparser.add_argument(
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
)

View file

@ -1,7 +1,6 @@
import argparse
from .. import __version__
from ..constants import * # NOQA
from ..helpers.argparsing import ArgumentParser
from ..remote import RemoteRepository
from ..logger import create_logger
@ -51,13 +50,5 @@ class VersionMixIn:
You can also use ``borg --version`` to display a potentially more precise client version.
"""
)
subparser = subparsers.add_parser(
"version",
parents=[common_parser],
add_help=False,
description=self.do_version.__doc__,
epilog=version_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help="display the Borg client and server versions",
)
subparser.set_defaults(func=self.do_version)
subparser = ArgumentParser(parents=[common_parser], description=self.do_version.__doc__, epilog=version_epilog)
subparsers.add_subcommand("version", subparser, help="display the Borg client and server versions")

View file

@ -2,12 +2,6 @@ from typing import Any, Type, Dict, Tuple
def get_compressor(name: str, **kwargs) -> Any: ...
class CompressionSpec:
def __init__(self, spec: str) -> None: ...
@property
def compressor(self) -> Any: ...
inner: CompressionSpec
class Compressor:
def __init__(self, name: Any = ..., **kwargs) -> None: ...
def compress(self, meta: Dict, data: bytes) -> Tuple[Dict, bytes]: ...

View file

@ -15,10 +15,10 @@ which compressor has been used to compress the data and dispatch to the correct
decompressor.
"""
from argparse import ArgumentTypeError
import math
import random
from struct import Struct
import sys
import zlib
try:
@ -28,15 +28,13 @@ except ImportError:
from .constants import MAX_DATA_SIZE, ROBJ_FILE_STREAM
from .helpers import Buffer, DecompressionError
import sys
from .helpers.argparsing import ArgumentTypeError
if sys.version_info >= (3, 14):
from compression import zstd
else:
from backports import zstd
cdef extern from "lz4.h":
int LZ4_compress_default(const char* source, char* dest, int inputSize, int maxOutputSize) nogil
int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil
@ -120,7 +118,6 @@ cdef class CompressorBase:
else:
pass # raise ValueError("size not present and not in legacy mode")
cdef class DecidingCompressor(CompressorBase):
"""
base class for (de)compression classes that (based on an internal _decide
@ -188,7 +185,6 @@ class CNONE(CompressorBase):
self.check_fix_size(meta, data)
return meta, data
class LZ4(DecidingCompressor):
"""
raw LZ4 compression / decompression (liblz4).
@ -260,7 +256,6 @@ class LZ4(DecidingCompressor):
self.check_fix_size(meta, data)
return meta, data
class LZMA(DecidingCompressor):
"""
lzma compression / decompression
@ -355,7 +350,6 @@ class ZLIB(DecidingCompressor):
except zlib.error as e:
raise DecompressionError(str(e)) from None
class ZLIB_legacy(CompressorBase):
"""
zlib compression / decompression (python stdlib)
@ -402,7 +396,6 @@ class ZLIB_legacy(CompressorBase):
except zlib.error as e:
raise DecompressionError(str(e)) from None
class Auto(CompressorBase):
"""
Meta-Compressor that decides which compression to use based on LZ4's ratio.
@ -484,7 +477,6 @@ class Auto(CompressorBase):
def detect(cls, data):
raise NotImplementedError
class ObfuscateSize(CompressorBase):
"""
Meta-Compressor that obfuscates the compressed data size.
@ -569,7 +561,6 @@ class ObfuscateSize(CompressorBase):
self.compressor = compressor_cls()
return self.compressor.decompress(meta, compressed_data) # decompress data
# Maps valid compressor names to their class
COMPRESSOR_TABLE = {
CNONE.name: CNONE,
@ -623,64 +614,3 @@ class Compressor:
return cls, (255 if cls.name == 'zlib_legacy' else level)
else:
raise ValueError('No decompressor for this data found: %r.', data[:2])
class CompressionSpec:
def __init__(self, s):
values = s.split(',')
count = len(values)
if count < 1:
raise ArgumentTypeError("not enough arguments")
# --compression algo[,level]
self.name = values[0]
if self.name in ('none', 'lz4', ):
return
elif self.name in ('zlib', 'lzma', 'zlib_legacy'): # zlib_legacy just for testing
if count < 2:
level = 6 # default compression level in py stdlib
elif count == 2:
level = int(values[1])
if not 0 <= level <= 9:
raise ArgumentTypeError("level must be >= 0 and <= 9")
else:
raise ArgumentTypeError("too many arguments")
self.level = level
elif self.name in ('zstd', ):
if count < 2:
level = 3 # default compression level in zstd
elif count == 2:
level = int(values[1])
if not 1 <= level <= 22:
raise ArgumentTypeError("level must be >= 1 and <= 22")
else:
raise ArgumentTypeError("too many arguments")
self.level = level
elif self.name == 'auto':
if 2 <= count <= 3:
compression = ','.join(values[1:])
else:
raise ArgumentTypeError("bad arguments")
self.inner = CompressionSpec(compression)
elif self.name == 'obfuscate':
if 3 <= count <= 5:
level = int(values[1])
if not ((1 <= level <= 6) or (110 <= level <= 123) or (level == 250)):
raise ArgumentTypeError("level must be (inclusively) within 1...6, 110...123 or equal to 250")
self.level = level
compression = ','.join(values[2:])
else:
raise ArgumentTypeError("bad arguments")
self.inner = CompressionSpec(compression)
else:
raise ArgumentTypeError("unsupported compression type")
@property
def compressor(self):
if self.name in ('none', 'lz4', ):
return get_compressor(self.name)
elif self.name in ('zlib', 'lzma', 'zstd', 'zlib_legacy'):
return get_compressor(self.name, level=self.level)
elif self.name == 'auto':
return get_compressor(self.name, compressor=self.inner.compressor)
elif self.name == 'obfuscate':
return get_compressor(self.name, level=self.level, compressor=self.inner.compressor)

View file

@ -370,6 +370,7 @@ class FuseBackend:
t0 = time.perf_counter()
archive = Archive(self._manifest, archive_id)
strip_components = self._args.strip_components
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(self._args.patterns, self._args.paths)
hlm = HardLinkManager(id_type=bytes, info_type=str) # hlid -> path

View file

@ -25,13 +25,14 @@ from .fs import O_, flags_dir, flags_special_follow, flags_special, flags_base,
from .fs import HardLinkManager
from .misc import sysinfo, log_multi, consume
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode
from .parseformat import octal_int, bin_to_hex, hex_to_bin, safe_encode, safe_decode
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval
from .parseformat import eval_escapes, decode_dict, interval
from .parseformat import (
PathSpec,
FilesystemPathSpec,
SortBySpec,
CompressionSpec,
ChunkerParams,
FilesCacheMode,
partial_format,

View file

@ -0,0 +1,152 @@
"""
Borg argument-parsing layer
===========================
All imports of ``ArgumentParser``, ``Namespace``, ``SUPPRESS``, etc. come
from this module. It is the single seam between borg and the underlying
parser library (jsonargparse).
Library choice
--------------
Borg uses **jsonargparse** instead of plain argparse. jsonargparse is a
superset of argparse that additionally supports:
* reading arguments from YAML/JSON config files (``--config``)
* reading arguments from environment variables
* nested namespaces for subcommands (each subcommand's arguments live in
their own ``Namespace`` object rather than the flat top-level namespace)
Parser hierarchy
----------------
Borg's command line has up to three levels::
borg [common-opts] <command> [common-opts] [<subcommand> [common-opts] [args]]
e.g. borg --info create ...
borg create --info ...
borg debug info --debug ...
Three ``ArgumentParser`` instances are constructed in ``build_parser()``:
``parser`` (top-level)
The root parser. Common options are registered here **with real
defaults** (``provide_defaults=True``).
``common_parser``
A helper parser (``add_help=False``) passed as ``parents=[common_parser]``
to every *leaf* subcommand parser (e.g. ``create``, ``repo-create``, ).
Common options are registered here **with** ``default=SUPPRESS`` so that
an option not given on the command line leaves no attribute at all in the
subcommand namespace.
``mid_common_parser``
Same as ``common_parser`` but used as the parent for *group* subcommand
parsers that introduce a second level (e.g. ``debug``, ``key``,
``benchmark``). Their *leaf* subcommand parsers also use
``mid_common_parser`` as a parent.
Common options (``--info``, ``--debug``, ``--repo``, ``--lock-wait``, )
are managed by ``Archiver.CommonOptions``, which calls
``define_common_options()`` once per parser so the same options appear at
every level with identical ``dest`` names.
Namespace flattening and precedence
-------------------------------------
jsonargparse stores each subcommand's parsed values in a nested
``Namespace`` object::
# borg --info create --debug ...
Namespace(
log_level = "info", # top-level
subcommand = "create",
create = Namespace(
log_level = "debug", # subcommand level
...
)
)
After ``parser.parse_args()`` returns, ``flatten_namespace()`` collapses
this tree into a single ``Namespace`` that borg's dispatch and command
implementations expect.
Precedence rule: the **most-specific** (innermost) value wins.
``flatten_namespace`` uses ``Namespace.as_flat()`` (provided by jsonargparse)
to linearise the nested tree into a flat dict with dotted keys encoding
depth, for example::
log_level = "info" # top-level (0 dots)
create.log_level = "debug" # one level deep (1 dot)
debug.info.log_level = "critical" # two levels deep (2 dots)
The entries are then sorted deepest-first so the most-specific value is
encountered first and wins. Shallower values only fill in if the key
has not been set yet.
Special case append-action options (e.g. ``--debug-topic``):
If a key already holds a list and the outer level also supplies a list,
the two lists are **merged** (outer values first, inner values last) so
that ``borg --debug-topic foo create --debug-topic bar`` accumulates
``["foo", "bar"]`` rather than losing one of the values.
The ``SUPPRESS`` default on sub-parsers is essential: if a common option
is not given at the subcommand level, it simply produces no attribute in
the subcommand namespace and the outer (top-level) default flows through
unchanged.
"""
from typing import Any
# here are the only imports from argparse and jsonargparse,
# all other imports of these names import them from here:
from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401
from jsonargparse import ArgumentParser as _ArgumentParser # we subclass that to add custom behavior
from jsonargparse import Namespace, ActionSubCommands, SUPPRESS, REMAINDER # noqa: F401
from jsonargparse.typing import register_type, PositiveInt # noqa: F401
class ArgumentParser(_ArgumentParser):
# the borg code always uses RawDescriptionHelpFormatter and add_help=False:
def __init__(self, *args, formatter_class=RawDescriptionHelpFormatter, add_help=False, **kwargs):
super().__init__(*args, formatter_class=formatter_class, add_help=add_help, **kwargs)
def flatten_namespace(ns: Any) -> Namespace:
"""
Flattens the nested namespace jsonargparse produces for subcommands into a
single-level namespace that borg's dispatch and command implementations expect.
Inner (subcommand) values take precedence over outer (top-level) values.
For list-typed values (append-action options like --debug-topic) that appear
at multiple levels, the lists are merged: outer values first, inner values last.
"""
flat = Namespace()
# Extract the joined subcommand path from the nested namespace tree.
subcmds = []
current = ns
while current and hasattr(current, "subcommand") and current.subcommand:
subcmds.append(current.subcommand)
current = getattr(current, current.subcommand, None)
if subcmds:
flat.subcommand = " ".join(subcmds)
# as_flat() linearises the nested tree into dotted-key entries, e.g.:
# log_level='info' (outer, 0 dots)
# create.log_level='debug' (subcommand, 1 dot)
# debug.info.log_level='crit' (two-level subcommand, 2 dots)
# Sorting deepest-first ensures the most-specific value is processed first and therefore wins ("inner wins" rule).
all_items = sorted(vars(ns.as_flat()).items(), key=lambda kv: kv[0].count("."), reverse=True)
for dotted_key, value in all_items:
dest = dotted_key.rsplit(".", 1)[-1] # e.g. "create.log_level" -> "log_level"
if dest == "subcommand":
continue
existing = getattr(flat, dest, None)
if existing is None:
setattr(flat, dest, value)
elif isinstance(existing, list) and isinstance(value, list):
# Append-action options (e.g. --debug-topic): outer values come first.
setattr(flat, dest, list(value) + list(existing))
return flat

View file

@ -1,5 +1,4 @@
import abc
import argparse
import base64
import binascii
import hashlib
@ -22,8 +21,11 @@ from ..logger import create_logger
logger = create_logger()
import yaml
from .errors import Error
from .fs import get_keys_dir, make_path_safe, slashify
from .argparsing import Action, ArgumentError, ArgumentTypeError, register_type
from .msgpack import Timestamp
from .time import OutputTimestamp, format_time, safe_timestamp
from .. import __version__ as borg_version
@ -35,6 +37,12 @@ if TYPE_CHECKING:
from ..item import ItemDiff
def octal_int(s):
if isinstance(s, int):
return s
return int(s, 8)
def bin_to_hex(binary):
return binascii.hexlify(binary).decode("ascii")
@ -120,16 +128,10 @@ def decode_dict(d, keys, encoding="utf-8", errors="surrogateescape"):
return d
def positive_int_validator(value):
"""argparse type for positive integers."""
int_value = int(value)
if int_value <= 0:
raise argparse.ArgumentTypeError("A positive integer is required: %s" % value)
return int_value
def interval(s):
"""Convert a string representing a valid interval to a number of seconds."""
if isinstance(s, int):
return s
seconds_in_a_minute = 60
seconds_in_an_hour = 60 * seconds_in_a_minute
seconds_in_a_day = 24 * seconds_in_an_hour
@ -150,7 +152,7 @@ def interval(s):
number = s[:-1]
suffix = s[-1]
else:
raise argparse.ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}')
raise ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}')
try:
seconds = int(number) * multiplier[suffix]
@ -158,16 +160,96 @@ def interval(s):
seconds = -1
if seconds <= 0:
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
raise ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
return seconds
class CompressionSpec:
def __init__(self, s):
if isinstance(s, CompressionSpec):
self.__dict__.update(s.__dict__)
return
values = s.split(",")
count = len(values)
if count < 1:
raise ArgumentTypeError("not enough arguments")
# --compression algo[,level]
self.name = values[0]
if self.name in ("none", "lz4"):
return
elif self.name in ("zlib", "lzma", "zlib_legacy"): # zlib_legacy just for testing
if count < 2:
level = 6 # default compression level in py stdlib
elif count == 2:
level = int(values[1])
if not 0 <= level <= 9:
raise ArgumentTypeError("level must be >= 0 and <= 9")
else:
raise ArgumentTypeError("too many arguments")
self.level = level
elif self.name in ("zstd",):
if count < 2:
level = 3 # default compression level in zstd
elif count == 2:
level = int(values[1])
if not 1 <= level <= 22:
raise ArgumentTypeError("level must be >= 1 and <= 22")
else:
raise ArgumentTypeError("too many arguments")
self.level = level
elif self.name == "auto":
if 2 <= count <= 3:
compression = ",".join(values[1:])
else:
raise ArgumentTypeError("bad arguments")
self.inner = CompressionSpec(compression)
elif self.name == "obfuscate":
if 3 <= count <= 5:
level = int(values[1])
if not ((1 <= level <= 6) or (110 <= level <= 123) or (level == 250)):
raise ArgumentTypeError("level must be (inclusively) within 1...6, 110...123 or equal to 250")
self.level = level
compression = ",".join(values[2:])
else:
raise ArgumentTypeError("bad arguments")
self.inner = CompressionSpec(compression)
else:
raise ArgumentTypeError("unsupported compression type")
@property
def compressor(self):
from ..compress import get_compressor
if self.name in ("none", "lz4"):
return get_compressor(self.name)
elif self.name in ("zlib", "lzma", "zstd", "zlib_legacy"):
return get_compressor(self.name, level=self.level)
elif self.name == "auto":
return get_compressor(self.name, compressor=self.inner.compressor)
elif self.name == "obfuscate":
return get_compressor(self.name, level=self.level, compressor=self.inner.compressor)
def __str__(self):
if self.name in ("none", "lz4"):
return f"{self.name}"
elif self.name in ("zlib", "lzma", "zstd", "zlib_legacy"):
return f"{self.name},{self.level}"
elif self.name == "auto":
return f"auto,{self.inner}"
elif self.name == "obfuscate":
return f"obfuscate,{self.level},{self.inner}"
else:
raise ValueError(f"unsupported compression type: {self.name}")
def ChunkerParams(s):
if isinstance(s, (list, tuple)):
return tuple(s)
params = s.strip().split(",")
count = len(params)
if count == 0:
raise argparse.ArgumentTypeError("no chunker params given")
raise ArgumentTypeError("no chunker params given")
algo = params[0].lower()
if algo == CH_FAIL and count == 3:
block_size = int(params[1])
@ -182,61 +264,51 @@ def ChunkerParams(s):
# or in-memory chunk management.
# choose the block (chunk) size wisely: if you have a lot of data and you cut
# it into very small chunks, you are asking for trouble!
raise argparse.ArgumentTypeError("block_size must not be less than 64 Bytes")
raise ArgumentTypeError("block_size must not be less than 64 Bytes")
if block_size > MAX_DATA_SIZE or header_size > MAX_DATA_SIZE:
raise argparse.ArgumentTypeError(
"block_size and header_size must not exceed MAX_DATA_SIZE [%d]" % MAX_DATA_SIZE
)
raise ArgumentTypeError("block_size and header_size must not exceed MAX_DATA_SIZE [%d]" % MAX_DATA_SIZE)
return algo, block_size, header_size
if algo == "default" and count == 1: # default
return CHUNKER_PARAMS
if algo == CH_BUZHASH64 and count == 5: # buzhash64, chunk_min, chunk_max, chunk_mask, window_size
chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[1:])
if not (chunk_min <= chunk_mask <= chunk_max):
raise argparse.ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max")
raise ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max")
if chunk_min < 6:
# see comment in 'fixed' algo check
raise argparse.ArgumentTypeError(
"min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)"
)
raise ArgumentTypeError("min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)")
if chunk_max > 23:
raise argparse.ArgumentTypeError(
"max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)"
)
raise ArgumentTypeError("max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)")
# note that for buzhash64, there is no problem with even window_size.
return CH_BUZHASH64, chunk_min, chunk_max, chunk_mask, window_size
# this must stay last as it deals with old-style compat mode (no algorithm, 4 params, buzhash):
if algo == CH_BUZHASH and count == 5 or count == 4: # [buzhash, ]chunk_min, chunk_max, chunk_mask, window_size
chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[count - 4 :])
if not (chunk_min <= chunk_mask <= chunk_max):
raise argparse.ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max")
raise ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max")
if chunk_min < 6:
# see comment in 'fixed' algo check
raise argparse.ArgumentTypeError(
"min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)"
)
raise ArgumentTypeError("min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)")
if chunk_max > 23:
raise argparse.ArgumentTypeError(
"max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)"
)
raise ArgumentTypeError("max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)")
if window_size % 2 == 0:
raise argparse.ArgumentTypeError("window_size must be an uneven (odd) number")
raise ArgumentTypeError("window_size must be an uneven (odd) number")
return CH_BUZHASH, chunk_min, chunk_max, chunk_mask, window_size
raise argparse.ArgumentTypeError("invalid chunker params")
raise ArgumentTypeError("invalid chunker params")
def FilesCacheMode(s):
ENTRIES_MAP = dict(ctime="c", mtime="m", size="s", inode="i", rechunk="r", disabled="d")
VALID_MODES = ("cis", "ims", "cs", "ms", "cr", "mr", "d", "s") # letters in alpha order
if s in VALID_MODES:
return s
entries = set(s.strip().split(","))
if not entries <= set(ENTRIES_MAP):
raise argparse.ArgumentTypeError(
"cache mode must be a comma-separated list of: %s" % ",".join(sorted(ENTRIES_MAP))
)
raise ArgumentTypeError("cache mode must be a comma-separated list of: %s" % ",".join(sorted(ENTRIES_MAP)))
short_entries = {ENTRIES_MAP[entry] for entry in entries}
mode = "".join(sorted(short_entries))
if mode not in VALID_MODES:
raise argparse.ArgumentTypeError("cache mode short must be one of: %s" % ",".join(VALID_MODES))
raise ArgumentTypeError("cache mode short must be one of: %s" % ",".join(VALID_MODES))
return mode
@ -332,22 +404,22 @@ replace_placeholders = PlaceholderReplacer()
def PathSpec(text):
if not text:
raise argparse.ArgumentTypeError("Empty strings are not accepted as paths.")
raise ArgumentTypeError("Empty strings are not accepted as paths.")
return text
def FilesystemPathSpec(text):
if not text:
raise argparse.ArgumentTypeError("Empty strings are not accepted as paths.")
raise ArgumentTypeError("Empty strings are not accepted as paths.")
return slashify(text)
def SortBySpec(text):
from ..manifest import AI_HUMAN_SORT_KEYS
for token in text.split(","):
if token not in AI_HUMAN_SORT_KEYS:
raise argparse.ArgumentTypeError("Invalid sort key: %s" % token)
for sort_key in text.split(","):
if sort_key not in AI_HUMAN_SORT_KEYS and sort_key != "ts": # idempotency: do not reject ts
raise ArgumentTypeError("Invalid sort key: %s" % sort_key)
return text.replace("timestamp", "ts").replace("archive", "name")
@ -369,6 +441,8 @@ class FileSize(int):
def parse_file_size(s):
"""Return int from file size (1234, 55G, 1.7T)."""
if isinstance(s, int):
return s
if not s:
return int(s) # will raise
s = s.upper()
@ -507,6 +581,9 @@ class Location:
local_re = re.compile(local_path_re, re.VERBOSE)
def __init__(self, text="", overrides={}, other=False):
if isinstance(text, Location):
self.__dict__.update(text.__dict__)
return
self.repo_env_var = "BORG_OTHER_REPO" if other else "BORG_REPO"
self.valid = False
self.proto = None
@ -632,22 +709,34 @@ def location_validator(proto=None, other=False):
try:
loc = Location(text, other=other)
except ValueError as err:
raise argparse.ArgumentTypeError(str(err)) from None
raise ArgumentTypeError(str(err)) from None
if proto is not None and loc.proto != proto:
if proto == "file":
raise argparse.ArgumentTypeError('"%s": Repository must be local' % text)
raise ArgumentTypeError('"%s": Repository must be local' % text)
else:
raise argparse.ArgumentTypeError('"%s": Repository must be remote' % text)
raise ArgumentTypeError('"%s": Repository must be remote' % text)
return loc
return validator
# Register types with jsonargparse so they can be represented in config files
# (e.g. for --print_config). Two things are needed:
# 1. A YAML representer so yaml.safe_dump can serialize Location objects to strings.
# 2. A jsonargparse register_type so it knows how to deserialize strings back to Location.
yaml.SafeDumper.add_representer(Location, lambda dumper, loc: dumper.represent_str(loc.raw or ""))
register_type(Location, serializer=lambda loc: loc.raw or "")
yaml.SafeDumper.add_representer(CompressionSpec, lambda dumper, cs: dumper.represent_str(str(cs)))
register_type(CompressionSpec)
def relative_time_marker_validator(text: str):
time_marker_regex = r"^\d+[ymwdHMS]$"
match = re.compile(time_marker_regex).search(text)
if not match:
raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S")
raise ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S")
else:
return text
@ -656,22 +745,20 @@ def text_validator(*, name, max_length, min_length=0, invalid_ctrl_chars="\0", i
def validator(text):
assert isinstance(text, str)
if len(text) < min_length:
raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length < {min_length}]')
raise ArgumentTypeError(f'Invalid {name}: "{text}" [length < {min_length}]')
if len(text) > max_length:
raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length > {max_length}]')
raise ArgumentTypeError(f'Invalid {name}: "{text}" [length > {max_length}]')
if invalid_ctrl_chars and re.search(f"[{re.escape(invalid_ctrl_chars)}]", text):
raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]')
raise ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]')
if invalid_chars and re.search(f"[{re.escape(invalid_chars)}]", text):
raise argparse.ArgumentTypeError(
f'Invalid {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]'
)
raise ArgumentTypeError(f'Invalid {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]')
if no_blanks and (text.startswith(" ") or text.endswith(" ")):
raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [leading or trailing blanks detected]')
raise ArgumentTypeError(f'Invalid {name}: "{text}" [leading or trailing blanks detected]')
try:
text.encode("utf-8", errors="strict")
except UnicodeEncodeError:
# looks like text contains surrogate-escapes
raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [contains non-unicode characters]')
raise ArgumentTypeError(f'Invalid {name}: "{text}" [contains non-unicode characters]')
return text
return validator
@ -1307,7 +1394,7 @@ def prepare_dump_dict(d):
return decode(d)
class Highlander(argparse.Action):
class Highlander(Action):
"""make sure some option is only given once"""
def __init__(self, *args, **kwargs):
@ -1316,7 +1403,7 @@ class Highlander(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if self.__called:
raise argparse.ArgumentError(self, "There can be only one.")
raise ArgumentError(self, "There can be only one.")
self.__called = True
setattr(namespace, self.dest, values)
@ -1326,7 +1413,7 @@ class MakePathSafeAction(Highlander):
try:
sanitized_path = make_path_safe(path)
except ValueError as e:
raise argparse.ArgumentError(self, e)
raise ArgumentError(self, e)
if sanitized_path == ".":
raise argparse.ArgumentError(self, f"{path!r} is not a valid file name")
raise ArgumentError(self, f"{path!r} is not a valid file name")
setattr(namespace, self.dest, sanitized_path)

View file

@ -37,6 +37,8 @@ def utcfromtimestampns(ts_ns: int) -> datetime:
def timestamp(s):
"""Convert a --timestamp=s argument to a datetime object."""
if isinstance(s, datetime):
return s
try:
# is it pointing to a file / directory?
ts_ns = safe_ns(os.stat(s).st_mtime_ns)

View file

@ -193,6 +193,7 @@ class FuseBackend:
archive = Archive(self._manifest, archive_id)
strip_components = self._args.strip_components
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
matcher = build_matcher(self._args.patterns, self._args.paths)
hlm = HardLinkManager(id_type=bytes, info_type=str)

View file

@ -1,4 +1,3 @@
import argparse
import fnmatch
import posixpath
import re
@ -8,6 +7,7 @@ from collections import namedtuple
from enum import Enum
from .helpers import clean_lines, shellpattern
from .helpers.argparsing import Action, ArgumentTypeError
from .helpers.errors import Error
@ -36,15 +36,15 @@ def load_exclude_file(fileobj, patterns):
patterns.append(parse_exclude_pattern(patternstr))
class ArgparsePatternAction(argparse.Action):
class ArgparsePatternAction(Action):
def __init__(self, nargs=1, **kw):
super().__init__(nargs=nargs, **kw)
def __call__(self, parser, args, values, option_string=None):
parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern)
parse_patternfile_line(values[0], args.pattern_roots, args.patterns, ShellPattern)
class ArgparsePatternFileAction(argparse.Action):
class ArgparsePatternFileAction(Action):
def __init__(self, nargs=1, **kw):
super().__init__(nargs=nargs, **kw)
@ -60,7 +60,7 @@ class ArgparsePatternFileAction(argparse.Action):
raise Error(str(e))
def parse(self, fobj, args):
load_pattern_file(fobj, args.paths, args.patterns)
load_pattern_file(fobj, args.pattern_roots, args.patterns)
class ArgparseExcludeFileAction(ArgparsePatternFileAction):
@ -357,16 +357,16 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern):
"p": IECommand.PatternStyle,
}
if not cmd_line_str:
raise argparse.ArgumentTypeError("A pattern/command must not be empty.")
raise ArgumentTypeError("A pattern/command must not be empty.")
cmd = cmd_prefix_map.get(cmd_line_str[0])
if cmd is None:
raise argparse.ArgumentTypeError("A pattern/command must start with any of: %s" % ", ".join(cmd_prefix_map))
raise ArgumentTypeError("A pattern/command must start with any of: %s" % ", ".join(cmd_prefix_map))
# remaining text on command-line following the command character
remainder_str = cmd_line_str[1:].lstrip()
if not remainder_str:
raise argparse.ArgumentTypeError("A pattern/command must have a value part.")
raise ArgumentTypeError("A pattern/command must have a value part.")
if cmd is IECommand.RootPath:
# TODO: validate string?
@ -376,7 +376,7 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern):
try:
val = get_pattern_class(remainder_str)
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid pattern style: {remainder_str}")
raise ArgumentTypeError(f"Invalid pattern style: {remainder_str}")
else:
# determine recurse_dir based on command type
recurse_dir = command_recurses_dir(cmd)

View file

@ -1,7 +1,7 @@
import argparse
import pytest
from . import Archiver, RK_ENCRYPTION, cmd
from ...helpers.argparsing import ArgumentParser, flatten_namespace
def test_bad_filters(archiver):
@ -93,45 +93,35 @@ class TestCommonOptions:
@pytest.fixture
def basic_parser(self):
parser = argparse.ArgumentParser(prog="test", description="test parser", add_help=False)
parser.common_options = Archiver.CommonOptions(
self.define_common_options, suffix_precedence=("_level0", "_level1")
)
parser = ArgumentParser(prog="test", description="test parser")
parser.common_options = Archiver.CommonOptions(self.define_common_options)
return parser
@pytest.fixture
def subparsers(self, basic_parser):
return basic_parser.add_subparsers(title="required arguments", metavar="<command>")
def subcommands(self, basic_parser):
return basic_parser.add_subcommands(required=False, title="required arguments", metavar="<command>")
@pytest.fixture
def parser(self, basic_parser):
basic_parser.common_options.add_common_group(basic_parser, "_level0", provide_defaults=True)
basic_parser.common_options.add_common_group(basic_parser, provide_defaults=True)
return basic_parser
@pytest.fixture
def common_parser(self, parser):
common_parser = argparse.ArgumentParser(add_help=False, prog="test")
parser.common_options.add_common_group(common_parser, "_level1")
common_parser = ArgumentParser(prog="test")
parser.common_options.add_common_group(common_parser)
return common_parser
@pytest.fixture
def parse_vars_from_line(self, parser, subparsers, common_parser):
subparser = subparsers.add_parser(
"subcommand",
parents=[common_parser],
add_help=False,
description="foo",
epilog="bar",
help="baz",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparser.set_defaults(func=1234)
def parse_vars_from_line(self, parser, subcommands, common_parser):
subparser = ArgumentParser(parents=[common_parser], description="foo", epilog="bar")
subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true")
subcommands.add_subcommand("subcmd", subparser, help="baz")
def parse_vars_from_line(*line):
print(line)
args = parser.parse_args(line)
parser.common_options.resolve(args)
args = flatten_namespace(args)
return vars(args)
return parse_vars_from_line
@ -144,25 +134,25 @@ class TestCommonOptions:
"progress": False,
}
assert parse_vars_from_line("--error", "subcommand", "--critical") == {
assert parse_vars_from_line("--error", "subcmd", "--critical") == {
"append": [],
"lock_wait": 1,
"log_level": "critical",
"progress": False,
"foo_bar": False,
"func": 1234,
"subcommand": "subcmd",
}
with pytest.raises(SystemExit):
parse_vars_from_line("--foo-bar", "subcommand")
parse_vars_from_line("--foo-bar", "subcmd")
assert parse_vars_from_line("--append=foo", "--append", "bar", "subcommand", "--append", "baz") == {
assert parse_vars_from_line("--append=foo", "--append", "bar", "subcmd", "--append", "baz") == {
"append": ["foo", "bar", "baz"],
"lock_wait": 1,
"log_level": "warning",
"progress": False,
"foo_bar": False,
"func": 1234,
"subcommand": "subcmd",
}
@pytest.mark.parametrize("position", ("before", "after", "both"))
@ -171,7 +161,7 @@ class TestCommonOptions:
line = []
if position in ("before", "both"):
line.append(flag)
line.append("subcommand")
line.append("subcmd")
if position in ("after", "both"):
line.append(flag)
@ -181,7 +171,7 @@ class TestCommonOptions:
"log_level": "warning",
"progress": False,
"foo_bar": False,
"func": 1234,
"subcommand": "subcmd",
args_key: args_value,
}

View file

@ -418,6 +418,21 @@ def test_create_paths_from_command_missing_command(archivers, request):
assert output.endswith("No command given." + os.linesep)
@pytest.mark.skipif(is_win32, reason="shell patterns not supported on Windows")
def test_create_paths_from_shell_command(archivers, request):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
create_regular_file(archiver.input_path, "file1", size=1024 * 80)
create_regular_file(archiver.input_path, "file2", size=1024 * 80)
create_regular_file(archiver.input_path, "file3", size=1024 * 80)
input_data = "input/file1\ninput/file2\ninput/file3"
# Use a shell pipe to test that shell=True works correctly.
cmd(archiver, "create", "--paths-from-shell-command", "test", "--", f"echo '{input_data}' | head -n 2")
archive_list = cmd(archiver, "list", "test", "--json-lines")
paths = [json.loads(line)["path"] for line in archive_list.split("\n") if line]
assert paths == ["input/file1", "input/file2"]
def test_create_without_root(archivers, request):
"""test create without a root"""
archiver = request.getfixturevalue(archivers)

View file

@ -15,7 +15,7 @@ def test_tag_set(archivers, request):
assert "tags: aa." in output
output = cmd(archiver, "tag", "-a", "archive", "--set", "bb")
assert "tags: bb." in output
output = cmd(archiver, "tag", "-a", "archive", "--set", "bb", "--set", "aa")
output = cmd(archiver, "tag", "-a", "archive", "--set", "bb", "aa")
assert "tags: aa,bb." in output # sorted!
output = cmd(archiver, "tag", "-a", "archive", "--set", "")
assert "tags: ." in output # no tags!
@ -46,7 +46,7 @@ def test_tag_set_noclobber_special(archivers, request):
output = cmd(archiver, "tag", "-a", "archive", "--set", "clobber")
assert "tags: @PROT." in output
# it is possible though to use --set if the existing special tags are also given:
output = cmd(archiver, "tag", "-a", "archive", "--set", "noclobber", "--set", "@PROT")
output = cmd(archiver, "tag", "-a", "archive", "--set", "noclobber", "@PROT")
assert "tags: @PROT,noclobber." in output

View file

@ -1,11 +1,12 @@
import argparse
import os
import zlib
import pytest
from ..compress import get_compressor, Compressor, CompressionSpec, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto
from ..compress import get_compressor, Compressor, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto
from ..helpers import CompressionSpec
from ..constants import ROBJ_FILE_STREAM, ROBJ_ARCHIVE_META
from ..helpers.argparsing import ArgumentTypeError
DATA = b"fooooooooobaaaaaaaar" * 10
params = dict(name="zlib", level=6)
@ -209,7 +210,7 @@ def test_specified_compression_level(c_type, c_name, c_levels):
@pytest.mark.parametrize("invalid_spec", ["", "lzma,9,invalid", "invalid"])
def test_invalid_compression_level(invalid_spec):
with pytest.raises(argparse.ArgumentTypeError):
with pytest.raises(ArgumentTypeError):
CompressionSpec(invalid_spec)

View file

@ -1,11 +1,12 @@
import base64
import os
from argparse import ArgumentTypeError
from datetime import datetime, timezone
import pytest
from ...constants import * # NOQA
from ...helpers.argparsing import ArgumentTypeError
from ...helpers.parseformat import (
bin_to_hex,
binary_to_json,

View file

@ -1,10 +1,10 @@
import argparse
import io
import os.path
import sys
import pytest
from ..helpers.argparsing import ArgumentTypeError
from ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern
from ..patterns import load_exclude_file, load_pattern_file
from ..patterns import parse_pattern, PatternMatcher
@ -491,7 +491,7 @@ def test_load_invalid_patterns_from_file(tmpdir, lines):
with patternfile.open("wt") as fh:
fh.write("\n".join(lines))
filename = str(patternfile)
with pytest.raises(argparse.ArgumentTypeError):
with pytest.raises(ArgumentTypeError):
roots = []
inclexclpatterns = []
with open(filename) as f: