Skip to content

Commit fb6ce2d

Browse files
add support for yaml config files, default config
1 parent 8f51646 commit fb6ce2d

File tree

8 files changed

+103
-5
lines changed

8 files changed

+103
-5
lines changed

docs/usage/general/config.rst.inc

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
Configuration files
2+
~~~~~~~~~~~~~~~~~~~
3+
4+
Borg supports reading options from YAML configuration files. This is
5+
implemented via `jsonargparse <https://jsonargparse.readthedocs.io/>`_
6+
and works for all options that can also be set on the command line.
7+
8+
Default configuration file
9+
``$BORG_CONFIG_DIR/default.yaml`` is loaded automatically on every Borg
10+
invocation if it exists. You do not need to pass ``--config`` explicitly
11+
for this file.
12+
13+
``--config PATH``
14+
Load additional options from the YAML file at *PATH*.
15+
Options in this file take precedence over the default config file but are
16+
overridden by explicit command-line arguments. This option can be used
17+
multiple times, with later files overriding earlier ones.
18+
19+
``--print_config``
20+
Print the current effective configuration (all options in YAML format) to
21+
stdout and exit. This reflects the merged result of the default config
22+
file, any ``--config`` file, environment variables, and command-line
23+
arguments given before ``--print_config``. The output can be used as a
24+
starting point for a config file.
25+
26+
File format
27+
Config files are YAML documents. Top-level keys are option names
28+
(without leading ``--`` and with ``-`` replaced by ``_``).
29+
Nested keys correspond to subcommands.
30+
31+
Example ``default.yaml``::
32+
33+
# apply to all borg commands:
34+
log_level: info
35+
show_rc: true
36+
37+
# options specific to "borg create":
38+
create:
39+
compression: zstd,3
40+
stats: true
41+
42+
The top-level keys set options that are common to all commands (equivalent
43+
to placing them before the subcommand on the command line). Keys nested
44+
under a subcommand name (e.g. ``create:``) are only applied when that
45+
subcommand is invoked.
46+
47+
Precedence (lowest to highest)
48+
1. Default config file (``$BORG_CONFIG_DIR/default.yaml``)
49+
2. ``--config`` file(s) (in the order given)
50+
3. Environment variables (e.g. ``BORG_REPO``)
51+
4. Command-line arguments
52+
53+
.. note::
54+
``--print_config`` shows the merged effective configuration and is a
55+
convenient way to check what values Borg will actually use, and to
56+
generate contents for your borg config file(s)::
57+
58+
borg --repo /backup/main create --compression zstd,3 --print_config

docs/usage/usage_general.rst.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

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

11+
.. _config:
12+
13+
.. include:: general/config.rst.inc
14+
1115
.. _env_vars:
1216

1317
.. include:: general/environment.rst.inc

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
"shtab>=1.8.0",
4141
"backports-zstd; python_version < '3.14'", # for python < 3.14.
4242
"jsonargparse @ git+https://github.com/omni-us/jsonargparse.git@main",
43+
"PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files
4344
]
4445

4546
[project.optional-dependencies]
@@ -259,7 +260,7 @@ deps = ["ruff"]
259260
commands = [["ruff", "check", "."]]
260261

261262
[tool.tox.env.mypy]
262-
deps = ["pytest", "mypy", "pkgconfig"]
263+
deps = ["pytest", "mypy", "pkgconfig", "types-PyYAML"]
263264
commands = [["mypy", "--ignore-missing-imports"]]
264265

265266
[tool.tox.env.docs]

requirements.d/development.lock.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ pytest-cov==7.0.0
1414
pytest-benchmark==5.2.3
1515
Cython==3.2.4
1616
pre-commit==4.5.1
17+
types-PyYAML==6.0.12.20250915

requirements.d/development.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ pytest-benchmark
1515
Cython
1616
pre-commit
1717
bandit[toml]
18+
types-PyYAML

src/borg/archiver/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from ..helpers import ErrorIgnoringTextIOWrapper
4646
from ..helpers import msgpack
4747
from ..helpers import sig_int
48+
from ..helpers import get_config_dir
4849
from ..remote import RemoteRepository
4950
from ..selftest import selftest
5051
except BaseException:
@@ -246,7 +247,12 @@ def add_argument(*args, **kwargs):
246247
def build_parser(self):
247248
from ._common import define_common_options
248249

249-
parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups")
250+
parser = ArgumentParser(
251+
prog=self.prog,
252+
description="Borg - Deduplicated Backups",
253+
default_config_files=[os.path.join(get_config_dir(), "default.yaml")],
254+
)
255+
parser.add_argument("--config", action="config")
250256
# paths and patterns must have an empty list as default everywhere
251257
parser.common_options = self.CommonOptions(define_common_options)
252258
parser.add_argument(

src/borg/helpers/argparsing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401
102102
from jsonargparse import ArgumentParser as _ArgumentParser # we subclass that to add custom behavior
103103
from jsonargparse import Namespace, SUPPRESS, REMAINDER # noqa: F401
104+
from jsonargparse.typing import register_type # noqa: F401
104105

105106
# borg completion uses these private symbols, so we need to import them:
106107
from jsonargparse._actions import _ActionSubCommands # noqa: F401

src/borg/helpers/parseformat.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121

2222
logger = create_logger()
2323

24+
import yaml
25+
2426
from .errors import Error
2527
from .fs import get_keys_dir, make_path_safe, slashify
26-
from .argparsing import Action, ArgumentError, ArgumentTypeError
28+
from .argparsing import Action, ArgumentError, ArgumentTypeError, register_type
2729
from .msgpack import Timestamp
2830
from .time import OutputTimestamp, format_time, safe_timestamp
2931
from .. import __version__ as borg_version
@@ -236,10 +238,22 @@ def compressor(self):
236238
elif self.name == "obfuscate":
237239
return get_compressor(self.name, level=self.level, compressor=self.inner.compressor)
238240

241+
def __str__(self):
242+
if self.name in ("none", "lz4"):
243+
return f"{self.name}"
244+
elif self.name in ("zlib", "lzma", "zstd", "zlib_legacy"):
245+
return f"{self.name},{self.level}"
246+
elif self.name == "auto":
247+
return f"auto,{self.inner}"
248+
elif self.name == "obfuscate":
249+
return f"obfuscate,{self.level},{self.inner}"
250+
else:
251+
raise ValueError(f"unsupported compression type: {self.name}")
252+
239253

240254
def ChunkerParams(s):
241-
if isinstance(s, tuple):
242-
return s
255+
if isinstance(s, (list, tuple)):
256+
return tuple(s)
243257
params = s.strip().split(",")
244258
count = len(params)
245259
if count == 0:
@@ -714,6 +728,18 @@ def validator(text):
714728
return validator
715729

716730

731+
# Register types with jsonargparse so they can be represented in config files
732+
# (e.g. for --print_config). Two things are needed:
733+
# 1. A YAML representer so yaml.safe_dump can serialize Location objects to strings.
734+
# 2. A jsonargparse register_type so it knows how to deserialize strings back to Location.
735+
736+
yaml.SafeDumper.add_representer(Location, lambda dumper, loc: dumper.represent_str(loc.raw or ""))
737+
register_type(Location, serializer=lambda loc: loc.raw or "")
738+
739+
yaml.SafeDumper.add_representer(CompressionSpec, lambda dumper, cs: dumper.represent_str(str(cs)))
740+
register_type(CompressionSpec)
741+
742+
717743
def relative_time_marker_validator(text: str):
718744
time_marker_regex = r"^\d+[ymwdHMS]$"
719745
match = re.compile(time_marker_regex).search(text)

0 commit comments

Comments
 (0)