add support for yaml config files, default config

This commit is contained in:
Thomas Waldmann 2026-02-27 14:52:20 +01:00
parent 2f778b974b
commit 63a45c6c21
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
8 changed files with 103 additions and 5 deletions

View file

@ -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

View file

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

View file

@ -41,6 +41,7 @@ dependencies = [
"backports-zstd; python_version < '3.14'", # for python < 3.14.
"xxhash>=2.0.0",
"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]
@ -260,7 +261,7 @@ deps = ["ruff"]
commands = [["ruff", "check", "."]]
[tool.tox.env.mypy]
deps = ["pytest", "mypy", "pkgconfig"]
deps = ["pytest", "mypy", "pkgconfig", "types-PyYAML"]
commands = [["mypy", "--ignore-missing-imports"]]
[tool.tox.env.docs]

View file

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

View file

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

View file

@ -45,6 +45,7 @@ try:
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:
@ -246,7 +247,12 @@ class Archiver(
def build_parser(self):
from ._common import define_common_options
parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups")
parser = ArgumentParser(
prog=self.prog,
description="Borg - Deduplicated Backups",
default_config_files=[os.path.join(get_config_dir(), "default.yaml")],
)
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(

View file

@ -101,6 +101,7 @@ from typing import Any
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, SUPPRESS, REMAINDER # noqa: F401
from jsonargparse.typing import register_type # noqa: F401
# borg completion uses these private symbols, so we need to import them:
from jsonargparse._actions import _ActionSubCommands # noqa: F401

View file

@ -21,9 +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
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
@ -236,10 +238,22 @@ class CompressionSpec:
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, tuple):
return s
if isinstance(s, (list, tuple)):
return tuple(s)
params = s.strip().split(",")
count = len(params)
if count == 0:
@ -714,6 +728,18 @@ def location_validator(proto=None, other=False):
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)