mirror of
https://github.com/borgbackup/borg.git
synced 2026-06-11 01:41:57 -04:00
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
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:
commit
3d437d8589
57 changed files with 820 additions and 937 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
64
docs/usage/general/config.rst.inc
Normal file
64
docs/usage/general/config.rst.inc
Normal 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
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
|
||||
.. include:: general/return-codes.rst.inc
|
||||
|
||||
.. _config:
|
||||
|
||||
.. include:: general/config.rst.inc
|
||||
|
||||
.. _env_vars:
|
||||
|
||||
.. include:: general/environment.rst.inc
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ pytest-benchmark
|
|||
Cython
|
||||
pre-commit
|
||||
bandit[toml]
|
||||
types-PyYAML
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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, ...)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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, ...)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]: ...
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
152
src/borg/helpers/argparsing.py
Normal file
152
src/borg/helpers/argparsing.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue