NAME

update_config - Deploy configuration fragments (dotfiles, etc.) locally or on remote hosts.

SYNOPSIS

update_config [--test] [--help] [--diff] [--silent] [--list] [--dir directory] [--root|--no-root] [--export] [--pack]

DESCRIPTION

The program reads configuration fragments from the ~/.update_config directory if it exists. Otherwise, it reads them from the $XDG_CONFIG_HOME/update_config directory (~/.config/update_config when XDG_CONFIG_HOME is not set). In both cases, all the subdirectories are read too, as well as the fragments embedded in the script itself by the --pack option. A fragment from the directory replaces an embedded fragment with the same name.

The non-core modules used by the program are installed automatically when they are missing (some of them only when they are first needed), with cpanm if it is available, or cpan otherwise. If the program is installed in the directories of the Perl installation (e.g. with sudo cpanm), the modules are installed there too, using sudo if needed. Otherwise, they are installed with local::lib if that module is available, or system-wide (using sudo if needed).

OPTIONS

The available options for the program are the following. They can all be abbreviated down to a single character.

--diff   Print the difference between the current configuration and what
         will be written

--silent Do not print the status of the configuration files (except for
         error messages).

--test   Do not modify the configuration files.

--list   Only print a list of the configuration files and exit.

--root   Install only the configuration fragments that are marked as
         'NEED ROOT'.

--no-root  Do not attempt to install the 'NEED ROOT' fragments.

--export Export in the current directory all the files that are embedded
         in the script.

--dir directory   Process only the configuration files in the given
         directory and its subdirectories, instead of those from the
         default path.

--pack  Write on the standard output a version of the script where all the
        config fragments are embedded. To be used typically with:
          update_config --pack | ssh user@host perl -- - --no-root
        or just
          update_config --pack | ssh host perl

--help   Print this help message.

FRAGMENT FILES

Each fragment file describes a piece of content that should be present in a target file. By default, the target is a file directly in your home directory, with the same name as the fragment. However, hidden files are ignored when the fragments are read, so most of the time you will set the target explicitly with the FILENAME directive described below (e.g. to update your .bashrc).

You can organize the fragments in sub-directories, but these are not part of the target path: only the base name of a fragment is used as its default target. A suffix between brackets in that name is not part of the target but it is the default guard suffix of the fragment (e.g. bashrc [linux] and bashrc [macos] both target .bashrc, with the linux and macos guard suffixes). Several fragments can be applied to the same file only if they have different guard suffixes (see the GUARD SUFFIX directive) or if they all use MERGE, otherwise the program fails without applying any fragment.

Similarly, a suffix between brackets at the end of the name of a directory (e.g. [linux] or work [linux]) is the default guard suffix of all the fragments that it contains, including in its sub-directories. When several of these suffixes apply to a fragment (from its directories or its own name), they are joined with -, from the outermost one (e.g. the default guard suffix of [linux]/shell [work]/bashrc [prompt] is linux-work-prompt). Hidden directories are ignored too and fragments are processed in alphabetical order of their base name, ignoring case.

Unless the MERGE directive is used (see "Merged files"), the content of the fragment is written in the target file between two guard lines, which look like ###AUTO-CONFIG START### and ###AUTO-CONFIG END###. Everything outside of these guards is left untouched, so you can still edit the rest of the file by hand. If the guards are not already present, the content is added at the end of the file, which is created if needed. When the guarded content is already up to date, the file is not modified at all.

Directives

A fragment can start with a few directives, one per line, that control how it is applied. The content of the fragment starts at the first line that is not a directive, and the empty lines following the directives are skipped. The supported directives are the following:

FILENAME path

Sets the target file, instead of using the name of the fragment. The path is relative to your home directory, unless it is an absolute path. The path can also be made of two commands separated by a comma. In that case, the current content is read from the output of the first command and the new content is passed on the input of the second one (this is useful for crontab -l, crontab -).

DIR path

Sets the directory of the target file, which keeps the name of the fragment (or the one given by FILENAME, which must then be a relative path). The path is relative to your home directory, unless it is an absolute path. This directive cannot be used when the target is made of two commands.

COMMENTS CHAR char

Sets the character that starts a comment in the target file, which is used in the guard lines (it is repeated three times). The default is #.

GUARD SUFFIX suffix

Adds a suffix to the guard lines, which then look like ###AUTO-CONFIG:suffix START###. You need this if several fragments target the same file. By default, the suffixes between brackets at the end of the name of the fragment and of its directories are used, if any (see above). With MERGE, there are no guard lines and the suffix is ignored.

IS PREFIX

Adds the content at the beginning of the file instead of at the end, when the guards are not already present.

ENTIRE FILE

Replaces the entire target file with the content of the fragment, without any guard lines.

MAKE EXECUTABLE

Makes the target file executable.

MERGE [format]

Merges the data of the fragment into the target file instead of using guard lines, see "Merged files" below. The format is one of json, toml, yaml or ini (case insensitive). Without a format, it is given by the extension of the target, which must be the last part of its name, ignoring the suffix between brackets (e.g. a fragment named config.toml [work] is merged as toml). The yml extension is recognized too. This directive cannot be used with COMMENTS CHAR, ENTIRE FILE or IS PREFIX.

MERGE ARRAYS mode

With MERGE, sets how the arrays without an explicit mode are merged (see "Merged files"): union or replace for all of them, or auto (the default) to use replace for the arrays that contain only scalars and union for the others.

PERM mode

Sets the permissions of the target file, given in octal (e.g. 600).

NEED ROOT

Marks a fragment that must be written as root (typically a file under /etc). These fragments are applied at the end of the run, by executing the program again through sudo with the --root option.

REQUIRE expression [# reason]

Evaluates a Perl expression and skips the fragment if it is false. The optional reason is printed when the fragment is skipped. The expression can use the which function from File::Which as well as the variables from English. This directive can be repeated, in which case all the expressions must be true.

POST UPDATE command

Executes a shell command after the target file has been updated. In the command, $@ is replaced by the path of the target file.

REM text

A comment, which is ignored.

Merged files

Some programs rewrite their own configuration file (re-ordering its entries, removing its comments, etc.), which would break the guard lines. For these files, the MERGE directive merges the data of the fragment, written in the same format as the target file, into the existing data of that file:

  • Maps (TOML tables, JSON objects, YAML mappings or INI sections), starting with the root of the file, are merged recursively: each key of the fragment is added to the existing map. For other values, the value from the fragment replaces the existing one.

  • Arrays are merged according to their mode, which is given by an update_config_arrays map placed next to them in the fragment (that is, in the map containing the arrays). Each key of that map is the name of an array and its value is one of:

    replace

    The array from the fragment replaces the existing one.

    union

    The elements of the fragment (which can be whole maps, e.g. TOML [[array]] entries) are appended to the existing array, unless an equal element is already present.

    key:field[,field...]

    For arrays of maps, the entries of the fragment are matched with the existing entries having the same value for all the given fields. A matching entry is merged like any other map (so values added there by the program are kept) and the entries without a match are appended.

    The update_config_arrays maps are not written in the target file. The arrays not listed in such a map use the mode given by the MERGE ARRAYS directive (by default, replace for arrays of scalars and union for the others).

  • The target file is entirely rewritten, with sorted keys and without its comments. But it is not modified at all if it already contains the data of the fragment, whatever its formatting.

  • The last applied version of each fragment (for each target) is saved in $XDG_STATE_HOME/update_config (~/.local/state/update_config by default), or in /var/lib/update_config for the NEED ROOT fragments (this can be overridden with the UPDATE_CONFIG_ROOT_STATE_DIR environment variable). On the next run, the values that were removed from the fragment are also removed from the target file, unless they were modified there. This also applies to the elements of arrays merged with union, and to the values of the entries of arrays merged with a key (such an entry is removed entirely if only its key fields remain). This is a best effort mechanism that does not cover the changes made before the state was saved, nor the fragments that were renamed or moved to another directory since then.

The ini format supports key = value lines, optionally in [section] blocks, and comment lines starting with ; or #. The values are kept as raw text. The json format accepts trailing commas and # comments but not // comments.

Examples

This fragment adds some content to your ~/.bashrc file, but only on WSL hosts. The guard suffix allows to have another fragment for the same file:

FILENAME .bashrc
GUARD SUFFIX WSL
REQUIRE `uname -r` =~ /WSL/ # Not a WSL host

export BROWSER=wslview

This one installs a full script in a system directory, and needs root to do so:

FILENAME /etc/network/if-pre-up.d/10-iptables-load
REQUIRE -d '/etc/network/if-pre-up.d' # No if-pre-up.d directory
NEED ROOT
ENTIRE FILE
MAKE EXECUTABLE

#!/bin/sh
iptables-restore < /etc/iptables.rules

And this one updates a configuration file and then reloads it in the running program:

FILENAME .tmux.conf
REQUIRE which('tmux') # tmux is not installed
POST UPDATE tmux source-file $@

set -g prefix C-a

The following fragments use the MERGE directive, for files that are rewritten by their program. This one sets a theme and adds a server to a TOML file, while keeping the other servers configured there (by default, the [[server]] entries are merged with union):

FILENAME .config/app/config.toml
MERGE toml

[ui]
theme = "dark"

[[server]]
name = "home"
url = "https://home.example.com"

This one configures some languages in the Helix editor. The entries of the language array are matched by their name: if a rust entry already exists, its indent is updated and its other values are kept. The marker for an array at the root of the file must be placed before any table header:

FILENAME .config/helix/languages.toml
MERGE toml

update_config_arrays = { language = "key:name" }

[[language]]
name = "rust"
indent = { tab-width = 4, unit = "    " }

[[language]]
name = "python"
auto-format = true

This one adds a key binding to Alacritty, where the bindings are identified by both their key and mods fields. As the bindings array is in the keyboard table, the marker is placed in that table:

FILENAME .config/alacritty/alacritty.toml
MERGE toml

[keyboard]
update_config_arrays = { bindings = "key:key,mods" }

[[keyboard.bindings]]
key = "N"
mods = "Control|Shift"
action = "CreateNewWindow"

And this one adds a list of excluded paths in a JSON file, without removing the ones that were already present (the array contains only scalars, so it would be replaced by default):

FILENAME .config/app/settings.json
MERGE json

{
  "update_config_arrays": {"exclude": "union"},
  "exclude": ["node_modules", ".cache"]
}