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] [--force] [--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 directory can also contain a .update_config file, with directives that apply to all its fragments (see "Directory directives"). 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.

--force  Overwrite the targets of the 'ENTIRE FILE' fragments even if they
         were modified since they were last written by the program, and
         the files that are not symbolic links at the targets of the
         'SYMLINK' fragments. These targets are then reported as
         "Updated: target (forced)".

--export Export in the current directory all the files that are embedded
         in the script, with their target and the directives that they
         inherit from their directories written explicitly.

--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 a dot_ prefix in the name of a fragment is replaced by a dot in its target (e.g. dot_bashrc targets .bashrc). The target can also be set explicitly with the FILENAME directive described below.

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 (unless the MIRROR directive is used). 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, which are always in upper case). 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 the directory given by DIR or MIRROR if any, itself relative to the base directory (your home directory, unless BASE is used). An absolute path, or a path starting with ~/ (relative to your home directory), ignores these directories. 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 the base directory (your home directory, unless BASE is used), unless it is an absolute path or it starts with ~ (for your home directory). This directive cannot be used when the target is made of two commands, nor with MIRROR.

BASE path

Sets the base directory of the target, which is your home directory by default: the DIR and FILENAME paths and the directories given by MIRROR are relative to it. A relative path is relative to the previous base directory, which is the one set by a BASE directive in a parent directory (see "Directory directives"), if any, or your home directory otherwise. The path can also be absolute, or start with ~ for a path relative to your home directory.

MIRROR

Uses the structure of the configuration directory for the target, which is mostly useful in a .update_config file (see "Directory directives"): the directory of the target is the path of the directory of the fragment, starting with the directory where MIRROR is set (the one containing the .update_config file, or the fragment itself), relative to the base directory (see BASE). For example, with MIRROR in dot_config/.update_config, the fragment dot_config/fish/config.fish targets ~/.config/fish/config.fish. The root of the configuration directory is never part of the mirrored path. As for the name of the fragment, the suffixes between brackets are removed from the names of the mirrored directories (a directory whose name is only such a suffix can't be mirrored) and a dot_ prefix is replaced by a dot (e.g. dot_config/app [work]/dot_rc targets .config/app/.rc). A DIR directive set deeper than MIRROR replaces the mirrored path (and the reverse), and FILENAME replaces the name of the fragment.

MIRROR CONTENTS

Like MIRROR, but the directory where the directive is set is not part of the mirrored path, only the directories below it are (e.g. with MIRROR CONTENTS in config/.update_config, the fragment config/fish/config.fish targets ~/fish/config.fish). The deepest of MIRROR and MIRROR CONTENTS wins.

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). The suffix is always converted to upper case, whether it comes from this directive or from the names of the files. 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. The last applied version of the fragment is saved (see "Merged files") and, if the target was modified since then, it is not overwritten: the program prints the modifications that would be lost, skips the fragment and continues with the other ones. The --force option overwrites the target anyway, and reports it as Updated: target (forced). A target that does not exist is always written.

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.

Makes the target a symbolic link to the given destination, instead of writing the content of the fragment, which must be empty. A destination starting with ~ is relative to your home directory, other relative destinations are relative to the directory of the link (as with ln -s). An existing symbolic link at the target is replaced. Other files are not overwritten (the fragment is skipped, the program continues with the other ones), unless the --force option is used. A directory is never replaced. The target can't be made of commands, and this directive can't be used in a .update_config file, nor with COMMENTS CHAR, ENTIRE FILE, IS PREFIX, MAKE EXECUTABLE, MERGE, MERGE ARRAYS or PERM (these directives are ignored when they are set in a parent directory).

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.

Directory directives

A directory of fragments can contain a .update_config file, with directives (and REM comments) on each line, possibly separated by empty lines, but no content. These directives apply to all the fragments of the directory, including those in its sub-directories. If several of these files apply to a fragment (from its directory and its parent directories), their directives are combined, starting with the outermost directory and ending with the directives of the fragment itself:

  • The REQUIRE directives are all evaluated, the fragment is skipped if any of them is false.

  • For the other directives, the value from the deepest file wins (so the directives of a fragment override those of its directories). Note that a directive without a value (e.g. ENTIRE FILE) cannot be disabled once it is set in a directory.

  • A GUARD SUFFIX directive in a .update_config file replaces the suffixes between brackets of the names of its directory and of the directories above it, but the suffixes of the directories below it and of the fragment itself are still added after it (e.g. with a .update_config file containing GUARD SUFFIX work in dir [d], the guard suffix of dir [d]/sub [s]/file is WORK-S). A GUARD SUFFIX directive in a fragment replaces all of these suffixes.

  • A COMMENTS CHAR directive is ignored by the fragments using MERGE when it is set in a parent directory of the one setting MERGE (otherwise, they cannot be used together). Similarly, MERGE ARRAYS can be set in a .update_config file without MERGE, it then only applies to the fragments that use MERGE.

  • The BASE directives are all applied, each relative to the previous one.

  • An absolute FILENAME (or a pair of commands, or a path starting with ~/) ignores the BASE, DIR, MIRROR and MIRROR CONTENTS directives set in a parent directory.

A FILENAME directive in a .update_config file sends all the fragments of the directory to the same target, which requires them to have different guard suffixes (e.g. from their names, like path [path] and editor [editor]).

For example, with a dot_config/.update_config file containing:

MIRROR

the fragment dot_config/fish/config.fish targets ~/.config/fish/config.fish, dot_config/git [work]/config targets ~/.config/git/config (with the WORK guard suffix) and dot_config/app/settings.json containing DIR .config/other targets ~/.config/other/settings.json. The same targets are obtained with a config/.update_config file containing:

BASE .config
MIRROR CONTENTS

and DIR other in config/app/settings.json.

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"]
}