Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
39636dc
make validators idempotent
ThomasWaldmann Feb 24, 2026
eabc473
streamline function names for borg key subcommands
ThomasWaldmann Feb 24, 2026
7e4b753
refactor: separate pattern roots from positional paths
ThomasWaldmann Feb 24, 2026
c149a35
migrate to jsonargparse
ThomasWaldmann Feb 24, 2026
ad21207
fix make build_man / build_usage
ThomasWaldmann Feb 25, 2026
68ebd68
SortBySpec: avoid triggering bandit security checker
ThomasWaldmann Feb 25, 2026
ec129ac
blacken src
ThomasWaldmann Feb 25, 2026
4e6c16e
import from jsonargparse: SUPPRESS, REMAINDER, NameSpace
ThomasWaldmann Feb 25, 2026
dd9ee3d
adapt borg completion for jsonargparse
ThomasWaldmann Feb 25, 2026
758c7b0
fix argparsing test
ThomasWaldmann Feb 25, 2026
361ba01
reorg imports
ThomasWaldmann Feb 25, 2026
15d5910
helpers.jap_helper -> helpers.argparsing
ThomasWaldmann Feb 25, 2026
c342c24
move compress.CompressionSpec to helpers.parseformat.CompressionSpec
ThomasWaldmann Feb 26, 2026
ad77c48
make build_usage / build_man: do not show options with SUPPRESS
ThomasWaldmann Feb 26, 2026
ceba422
reorg imports
ThomasWaldmann Feb 26, 2026
f951a12
add octal_int validator
ThomasWaldmann Feb 26, 2026
52b994d
archiver: replace CommonOptions suffix hack with jsonargparse-native …
ThomasWaldmann Feb 27, 2026
405d8ff
simplify flatten_namespace, add docstring for argparsing
ThomasWaldmann Feb 27, 2026
8f51646
argparsing: add ArgumentParser subclass with borg's usual defaults
ThomasWaldmann Feb 27, 2026
fb6ce2d
add support for yaml config files, default config
ThomasWaldmann Feb 27, 2026
e4e484f
add support for auto-generated environment variables (jsonargparse)
ThomasWaldmann Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/usage/general/config.rst.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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.

Precedence (lowest to highest)
1. Default config file (``$BORG_CONFIG_DIR/default.yaml``)
2. ``--config`` file(s) (in the order given)
3. Environment variables (e.g. ``BORG_REPO``)
4. Command-line arguments

.. 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
26 changes: 26 additions & 0 deletions docs/usage/general/environment.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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/
4 changes: 4 additions & 0 deletions docs/usage/usage_general.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

.. include:: general/return-codes.rst.inc

.. _config:

.. include:: general/config.rst.inc

.. _env_vars:

.. include:: general/environment.rst.inc
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies = [
"argon2-cffi",
"shtab>=1.8.0",
"backports-zstd; python_version < '3.14'", # for python < 3.14.
"jsonargparse @ git+https://github.com/omni-us/jsonargparse.git@main",
"PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files
]

[project.optional-dependencies]
Expand Down Expand Up @@ -258,7 +260,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]
Expand Down
1 change: 1 addition & 0 deletions requirements.d/development.lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions requirements.d/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ pytest-benchmark
Cython
pre-commit
bandit[toml]
types-PyYAML
58 changes: 33 additions & 25 deletions scripts/make.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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):
Expand Down Expand Up @@ -46,7 +47,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None):
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
Expand Down Expand Up @@ -100,17 +101,18 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None):
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
Expand All @@ -121,17 +123,18 @@ def is_positional_group(group):
# (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:
Expand Down Expand Up @@ -218,18 +221,19 @@ def write_row_separator():
)

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:
Expand All @@ -238,7 +242,7 @@ def is_positional_group(group):

opts = OrderedDict()

for option in group._group_actions:
for option in actions:
if option.metavar:
option_fmt = "%s " + option.metavar
else:
Expand Down Expand Up @@ -323,7 +327,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None):
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
Expand All @@ -349,7 +353,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None):

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}")
Expand Down Expand Up @@ -503,34 +507,38 @@ def ref_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
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:
Expand Down
3 changes: 1 addition & 2 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
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
Expand All @@ -35,7 +34,7 @@
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
Expand Down
Loading