Introduction
chezmoi_modify_manager
is an addon for chezmoi
that deals with settings files that contain a mix of settings and state.
So far handling INI-style files are supported.
A typical example of this is KDE settings files. These contain (apart from settings) state like recently opened files and positions of windows and dialog boxes. Other programs (such as PrusaSlicer) also do the same thing.
chezmoi_modify_manager
allows you to ignore certain sections of those
INI files when managing the configuration files with chezmoi.
Note! This documentation reflects the latest release and may not match older versions or newer in-development versions.
Features
- Ignore entire sections or specific keys in an INI style file.
- Ignore a key in a section based on regular expressions.
- Force set a value (useful together with templating).
- Force remove a section, key or entries matching a regex (useful together with templating).
- Apply a transformation to the value of a specified key. These are special
operations that are built in and provide more complicated transformations.
Some examples that this can do:
- Look up a password in the platform keyring
- Ignore the sorting order of a list style value (
key=a,b,c,d
) - etc.
- Assisted adding/updating of files in your chezmoi source state.
- Optional built in self-updater
Installation & upgrades
It is assumed you already have chezmoi set up and understand the basics of how it works.
- To your root
.chezmoiignore
add:**/*.src.ini
. These files should not be checked out into your target directory, but acts as the "source of truth" for the modify script. - Do one of these:
- Recommended: Install
chezmoi_modify_manager
into your$PATH
. This can be done by one of (in descending order of preference):- Using a distro package (if available for what you use)
- Download the binary from the releases on GitHub and install it somewhere into your
PATH
. - Install from [crates.io] using
cargo
(only do this if you know what you are doing).
- Not recommended: Install
chezmoi_modify_manager
from the releases page into<chezmoi-source-directory>/.utils/chezmoi_modify_manager-<os>-<arch>
where<os>
is typicallylinux
and<arch>
is typicallyx86-64
. If you use another path, the template modify script that is added will be wrong.
- Recommended: Install
- Run
chezmoi_modify_manager --doctor
and make sure it reports no major issues with your installation.
Tab completion
Optionally you can install tab completion. The tab completion can be generated
using the hidden command line flag --bpaf-complete-style-SHELL_NAME
, (e.g.
--bpaf-complete-style-zsh
, --bpaf-complete-style-bash
, ...). As this is
handled internally by the command line parsing library we use, please see
their documentation
for detailed instructions.
For the Arch Linux AUR package, the completions are already installed for you (except for elvish, which doesn't support a global install).
Upgrading
Depending on the installation method:
chezmoi_modify_manager --upgrade
- With your package manager
- For each OS and architecture, update the file
.utils/chezmoi_modify_manager-<os>-<arch>
. Note! For executables that you can run (i.e. the native one) you can still use--upgrade
to do this.
You are in control of updates. Nothing will happen unless you pass
--upgrade
. Consider subscribing to be notified of new releases on the GitHub repository. This can be done viaWatch
->Custom
in the top right corner on that page after logging in to GitHub. Or just remember to check with--upgrade
occasionally.
Basic Usage
Theory of operation
For each settings file you want to manage with chezmoi_modify_manager
there
will be two files in your chezmoi source directory:
modify_<config file>
ormodify_<config file>.tmpl
, e.g.modify_private_kdeglobals.tmpl
This is the modify script/configuration file that callschezmoi_modify_manager
. It contains the directives describing what to ignore.<config file>.src.ini
, e.g.private_kdeglobals.src.ini
This is the source state of the INI file.
The modify_
script is responsible for generating the new state of the file
given the current state in your home directory. The modify_
script is set
up to use chezmoi_modify_manager
as an interpreter to do so.
chezmoi_modify_manager
will read the modify script to read configuration and
the .src.ini
file and by default will apply that file exactly (ignoring blank
lines and comments).
However, by giving additional directives to chezmoi_modify_manager
in the
modify_
script you can tell it to ignore certain sections (see
chezmoi_modify_manager --help-syntax
for details). For example:
ignore "KFileDialog Settings" "Show Inline Previews"
ignore section "DirSelect Dialog"
will tell it to ignore the key Show Inline Previews
in the section
KFileDialog Settings
and the entire section DirSelect Dialog
. More on
this below.
Adding files
Always refer to
chezmoi_modify_manager --help
for the most up-to-date details that matches the version you are using.
There are two modes to add files in:
-s
/--smart-add
: Smart re-add mode that re-adds files as managed.src.ini
if they are already managed, otherwise adds with plain chezmoi.-a
/--add
: This adds or converts from plain chezmoi to managed.src.ini
.
Here are some examples:
# Add configs to be handled by chezmoi_modify_manager (or convert configs
# managed by chezmoi to be managed by chezmoi_modify_manager).
chezmoi_modify_manager --add ~/.config/kdeglobals ~/.config/kwinrc
# Re-add config after changes in the live system.
chezmoi_modify_manager --add ~/.config/kdeglobals
# Don't remember if chezmoi_modify_manager handles the file or if it is raw chezmoi?
# Use smart mode (-s/--smart-add) to update the file!
chezmoi_modify_manager --smart-add ~/.config/PrusaSlicer/PrusaSlicer.ini
In addition, you can control readding behaviour with some settings in the
modify_<config file>
, to filter out entries while readding. This is covered
in the next chapter.
Configuring filters
The file modify_<config file>
(or modify_<config file>.tmpl
if you wish
to use chezmoi templating) contain the control directives for that config
file which controls the behaviour for chezmoi apply
of those files (as well as
when readding files from the system). The full details on this file are in the
next chapter, this section just covers the basics.
A basic such file will have this structure:
#!/usr/bin/env chezmoi_modify_manager
source auto
# This is a comment
# The next line is a directive. Directives are delimited by newlines.
ignore "SomeSection" "SomeKey"
ignore section "An entire section that is ignored"
ignore regex "Some Sections .*" "A key regex .*"
This illustrates some basics:
- The first line needs to be a
#!
that tells the OS thatchezmoi_modify_manager
should be the interpreter for this file. (This still works on Windows becausechezmoi
handles that internally as far as I understand, though I don't use Windows myself.) - The
source
directive tellschezmoi_modify_manager
where to look for the.src.ini
file. As of chezmoi 2.46.1 this can be auto-detected. If you use an older version,chezmoi_modify_manager --add
will detect that and insert the appropriate template based line instead. - The
ignore
directive is the most important directive. It has two effects:- When running
chezmoi apply
it results in the matching entries from.src.ini
being ignored, and the current system state is used instead. - When running
chezmoi_modify_manager --add
(or--smart-add
) it results in not copying matching entries to the.src.ini
to begin with.
- When running
There are several other directives as well, here is a basic rundown of them, they are covered in more detail in the next chapter. Here is a short summary:
set
: Sets an entry to a specific value. Useful together with chezmoi templating.remove
: Remove a specific entry, also useful together with chezmoi templating.transform
: Apply a custom transformation to the value. Can be used to handle some hard to deal with corner cases, supported transforms are covered in a later chapter.add:hide
&add:remove
: Useful together with certain transforms to control re-adding behaviour.no-warn-multiple-key-matches
: If there are multiple regex rules that overlap a warning will be issued. You can use this directive to quieten those warnings if this is intentional. See action evaluation order for more information on this.
Syntax of configuration files
chezmoi_modify_manager uses basic configuration files to control how to
merge INI files. These are the modify_<config_file_name>
files. They can
also be templated with chezmoi
by naming the file
modify_<config_file_name>.tmpl
instead. The easiest way to get started is
to use -a
to add a file and generate a skeleton configuration file.
Syntax
The file consists of directives, one per line. Comments are supported by prefixing a line with #. Comments are only supported at the start of lines.
Note! If a key appears before the first section, use
<NO_SECTION>
as the section.
Note! The modify script can itself be a chezmoi template (if it ends with
.tmpl
), which can be useful if you want to do host specific configuration using theset
directive for example.
This however will slow things down every so slightly as chezmoi has to run its templating engine on the file. Typically, this will be an overhead of about half a millisecond per templated modify script (measured on an AMD Ryzen 5 5600X).
Directives
source
This directive is required. It specifies where to find the source file (i.e. the file in the dotfile repo). It should have the following format to support Chezmoi versions older than v2.46.1:
source "{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini"
From Chezmoi v2.46.1 and forward the following also works instead:
source auto
ignore
Ignore a certain line, always taking it from the target file (i.e. file in your home directory), instead of the source state. The following variants are supported:
ignore section "my-section"
ignore "my-section" "my-key"
ignore regex "section.*regex" "key regex.*"
- The first form ignores a whole section (exact literal match).
- The second form ignores a specific key (exact literal match).
- The third form uses a regex to ignore a specific key.
Prefer the exact literal match variants where possible, they will be marginally faster.
An additional effect is that lines that are missing in the source state will not be deleted if they are ignored.
Finally, ignored lines will not be added back when using --add
or
--smart-add
, in order to reduce git diffs.
set
Set an entry to a specific value. This is primarily useful together with chezmoi templates, allowing you to override a specific value for only some of your computers. The following variants are supported:
set "section" "key" "value"
set "section" "key" "value" separator="="
By default, separator is " = "
, which might not match what the program that
the ini files belongs to uses.
Notes:
- Only exact literal matches are supported.
- It works better if the line exists in the source & target state, otherwise it is likely the line will get formatted weirdly (which will often be changed by the program the INI file belongs to).
remove
Unconditionally remove everything matching the directive. This is primarily useful together with chezmoi templates, allowing you to remove a specific key or section for only some of your computers. The following variants are supported:
remove section "my-section"
remove "my-section" "my-key"
remove regex "section.*regex" "key regex.*"
(Matching works identically to ignore, see above for more details.)
transform
Some specific situations need more complicated merging that a simple ignore. For those situations you can use transforms. Supported variants are:
transform "section" "key" transform-name arg1="value" arg2="value" ...
transform regex "section-regex.*" "key-regex.*" transform-name arg1="value" ...
(Matching works identically to ignore except matching entire sections is not supported. See above for more details.)
For example, to treat mykey
in mysection
as an unsorted comma separated
list, you could use:
transform "mysection" "mykey" unsorted-list separator=","
The full list of supported transforms, and how to use them can be listed
using --help-transforms
.
add:remove & add:hide
These two directives control the behaviour when using --add or --smart-add. In particular, these allow filtering lines that will be added back to the source state.
add:remove
will remove the matching lines entirely. The following forms are
supported:
add:remove section "section name"
add:remove "section name" "key"
add:remove regex "section-regex.*" "key-regex.*"
(Matching works identically to ignore, see above for more details.)
add:hide
will instead keep the entries but replace the value associated with
those keys. This is useful together with the keyring transform in particular,
as the key needs to exist in the source or target state for it to trigger
the replacement. The following forms are supported:
add:hide section "section name"
add:hide "section name" "key"
add:hide regex "section-regex.*" "key-regex.*"
(Matching works identically to ignore, see above for more details.)
no-warn-multiple-key-matches
This directive quietens warnings on multiple regular expressions matching the same section+key. While the warning is generally useful, sometimes you might actually "know what you are doing" and want to suppress it.
Transforms
This is a list of supported transforms. These are used to support some special hard-to-handle cases. The general syntax is documented elsewhere, but in short:
transform "section" "key" transform-name arg1="value" arg2="value" ...
transform regex "section-regex.*" "key-regex.*" transform-name arg1="value" ...
For example:
transform "mysection" "mykey" unsorted-list separator=","
Below is a list of supported transforms, but remember to check
chezmoi_modify_manager --help-transforms
for the most up-to-date list.
unsorted-list
Compare the value as an unsorted list. Useful because Konversation likes to reorder lists.
Arguments:
separator=","
: Separating character between list elements
kde-shortcut
Specialised transform to handle KDE changing certain global shortcuts back and forth between formats like:
playmedia=none,,Play media playback
playmedia=none,none,Play media playback
No arguments.
keyring
Get the value for a key from the system keyring. Useful for passwords etc that you do not want in your dotfiles repo.
Arguments:
service="service-name"
: Service name to find entry in the keyring.user="user-name"
: Username to find entry in the keyring.
You can add an entry to the secret store for your platform with:
chezmoi_modify_manager --keyring-set service-name user-name
Examples
This chapter has examples of how to configure chezmoi_modify_manager
for
several different programs, as well as some general examples on more advanced
topics.
Examples: Ignores & transforms
Here are some useful examples of flags for various settings files I have come across.
KDE
dolphinrc
ignore section "MainWindow"
ignore section "KPropertiesDialog"
ignore "General" "ViewPropsTimestamp"
ignore "Open-with settings" "History"
kdeglobals
ignore "General" "ColorSchemeHash"
ignore "KFileDialog Settings" "Show hidden files"
ignore "KFileDialog Settings" "Show Inline Previews"
ignore section "DirSelect Dialog"
kglobalshortcutsrc
There are two issues in this configuration.
First, ActivityManager switch-to-activity entries. There are multiple entries, making it a perfect fit for a regular expression. Note that this is not state per se. It does however seem to vary between computers, having different UUID values.
Second, certain shortcut keys like flipping between two representations. A specialised transform has been added to handle this case. When this is needed you will see diffs like the following:
-playmedia=none,,Play media playback
+playmedia=none,none,Play media playback
In summary, the following seems to work well:
# The two regex below have overlapping matches, this is OK in this case so
# turn off the warning for this file.
no-warn-multiple-key-matches
ignore regex "ActivityManager" "switch-to-activity-.*"
transform regex ".*" ".*" kde-shortcut
konversationrc
Konversation has two relevant quirks:
- It saves the password in the settings file (instead of using kwallet)
- It resorts it alias list every time.
ignore "ServerListDialog" "Size"
transform "Aliases" "AliasList" unsorted-list separator=","
transform "Identity 0" "Password" keyring service="konversation" user="konversation_id0"
# Make sure the password isn't added back into the config file on re-add
add:hide "Identity 0" "Password"
To store the password for Identity 0 in your keyring of choice you can use:
$ chezmoi_modify_manager --keyring-set konversation konversation_id0
[Enter your password at the prompt]
kwinrc
Similar to kglobalshortcutsrc there are computer specific UUIDs. In addition, the tiling configurations seem to be overwritten by KDE Plasma between computers.
ignore regex "Desktops" "Id_.*"
ignore regex "Tiling\\]\\[.*" ".*"
plasmanotifyrc
ignore section "DoNotDisturb"
Trolltech.conf
This is a Qt config, rather than a KDE config (strictly speaking) but since KDE uses Qt, it is sitll relevant.
ignore "Qt" "filedialog"
PrusaSlicer / SuperSlicer
PrusaSlicer and the fork SuperSlicer also use INI style files:
PrusaSlicer.ini / SuperSlicer.ini
ignore "<NO_SECTION>" "auto_toolbar_size"
ignore "<NO_SECTION>" "downloader_url_registered"
ignore "<NO_SECTION>" "freecad_path"
ignore "<NO_SECTION>" "last_output_path_removable"
ignore "<NO_SECTION>" "last_output_path"
ignore "<NO_SECTION>" "version_online_seen"
ignore "<NO_SECTION>" "version_online"
ignore "<NO_SECTION>" "version_system_info_sent"
ignore "<NO_SECTION>" "version"
ignore "<NO_SECTION>" "window_mainframe"
ignore "font" "active_font"
ignore "presets" "filament"
ignore "presets" "print"
ignore "presets" "printer"
ignore "presets" "sla_material"
ignore "presets" "sla_print"
ignore regex "<NO_SECTION>" "desktop_integration_.*"
ignore regex "<NO_SECTION>" "print_host_queue_dialog_.*"
ignore regex "font:.*" ".*"
ignore regex "presets" "filament_.*"
ignore section "recent_projects"
ignore section "recent"
PrusaSlicerGcodeViewer.ini / SuperSlicerGcodeViewer.ini
ignore "<NO_SECTION>" "version"
ignore "<NO_SECTION>" "window_mainframe"
ignore section "recent_projects"
PrusaSlicer physical printer settings
PrusaSlicer allows you to configure "physical printers" (with connection details
to e.g. OctoPrint or PrusaLink). There will be one such config per physical printer
you configured, located at .config/PrusaSlicer/physical_printer/<my_printer_name>.ini
As these contain login details you probably want to put that in your keyring instead of in git. This works similarly to konversation.
For example, you might use the following if you have a Prusa Mk3.9:
transform "<NO_SECTION>" "printhost_password" keyring service="chezmoi_modify_manager" user="prusa_mk39_password" separator=" = "
transform "<NO_SECTION>" "printhost_apikey" keyring service="chezmoi_modify_manager" user="prusa_mk39_apikey" separator=" = "
add:hide "<NO_SECTION>" "printhost_password"
add:hide "<NO_SECTION>" "printhost_apikey"
To add your password and API key you would then use:
chezmoi_modify_manager --keyring-set chezmoi_modify_manager prusa_mk39_password
Password: [Enter password]
chezmoi_modify_manager --keyring-set chezmoi_modify_manager prusa_mk39_apikey
Password: [Enter the API key]
KeePassXC
keepassxc.ini
KeePassXC stores private and public keys for KeeShare in the config. You may not want to commit this to the repository.
ignore "KeeShare" "Active"
ignore "KeeShare" "Foreign"
ignore "KeeShare" "Own"
GTK-3.0/GTK-4.0
settings.ini
The file ~/.config/gtk-<version>/settings.ini
has a DPI value in it that
changes between computers. Thus, each of those setting files need the
following:
ignore "Settings" "gtk-xft-dpi"
Advanced examples: set, remove, add:*
set/remove
The set
and remove
directives are meant to be used together with templating
in the modify scripts. For example, there might be a key binding in KDE you only
want on computers were a specific program is installed. This could be accomplished
by something like the following for kglobalshortcutsrc
{{if lookPath "my-fancy-program"}}
set "my-fancy-program.desktop" _k_friendly_name "My fancy program" separator="="
set "my-fancy-program.desktop" _launch "Ctrl+Shift+Y,none,my-fancy-program" separator="="
{{end}}
# Make sure the lines aren't added back into the config for all systems
# This should be outside the if statement
add:remove "my-fancy-program.desktop" _k_friendly_name
add:remove "my-fancy-program.desktop" _launch
(In this case, note that you might need to manage the .desktop
file with
chezmoi as well. KDE normally creates these in $HOME/.local/share/applications/
.)
Similarly, remove
can be used to remove entries, but be careful when readding
the source files: If you blindly re-add the file on the computer where the lines
are filtered out, they will get lost for all computers.
add:remove/add:hide
The directives add:remove
and add:hide
can be used to remove entries and
hide values respectively when re-adding files from the system to the chezmoi
source state.
Some use cases for this are:
- Use
add:hide
to prevent a password from being added back to the source state when you re-add a file with other changes. See the konversationrc example for an example of this. By usingadd:hide
, the line will still be present in the source file, but without its value. This ensures that the keyring transform is able to find it in the source state and do its work when checking out the file on a new system. - Use
add:remove
to prevent a line from entering the source state at all. This can be useful together with system specific configuration with theset
directive:
This example for the{{ if (.is_work) }} set "Default Applications" "x-scheme-handler/jetbrains" "jetbrains-toolbox.desktop" separator="=" {{ end }} # Completely remove the line when adding back (regardless of which computer this is on). add:remove "Default Applications" "x-scheme-handler/jetbrains"
mimeapps.list
file will add a specific line only ifis_work
is true. Theadd:remove
directive helps prevent that line from being added back to the source state by mistake (where it would be applied to other computers unintentionally).
NOTE: The
add:hide
andadd:remove
directives are processed as is without going through chezmoi's template engine when re-adding files. This means it won't matter if they are inside an if block, nor can you use template expressions in their arguments.
NOTE:
ignore
directives also result in an implicitadd:remove
. Again, it doesn't matter if it is inside an if block or not currently during adding of files, and any template expressions will not be expanded.
Both of these limitations may change in the future.
Migration
This chapter cover migrations between major versions of chezmoi_modify_manager
.
Migration from version 1.x to 2.x
The new Rust code base has a superset of the features of the 1.x version, and it is also about 50x faster (in release builds, about 25x in debug builds).
However, there is some work involved in migrating:
- Different installation method.
- The syntax has changed in the modify scripts.
- Some changes to transforms (in particular the keyring transform has changed).
In addition, the following differences are good to know about:
- The separate shell script to help with adding files is gone, the functionality
is now built into the main program (see
--help
output). - For binary installs from GitHub, you can now use a built-in self updater
(
--upgrade
). - The regex syntax is different. Previously Python re module was used, now the regex crate for Rust is used. For most simple regular expressions there will be no relevant difference. However, some features (such as back references and look arounds) are not supported.
- Platform support with precompiled binaries is somewhat limited (compared to everything that Python supports). This is due to what I can build & test and what GitHub CI supports. Pull requests that enable testing and building for more platforms are welcome however (if the tests cannot be executed on GitHub CI, I will not accept it however).
Installation
The methods of installation are different. No longer do you need (or should) add this repo as a submodule in your dotfiles repo. Remove that and instead see the installation section in the README.
Modify scripts: Automatic conversion
There is a script
that can help if you have standard shaped files (i.e. as created by the old chezmoi_ini_add
).
However, it will not handle 100% of the conversion for transforms. The argument list format has changed, as have some of the argument names. See below for more details.
Also, special consideration needs to be taken for the keyring transform.
Modify scripts: Manual conversion
The first line should now be used to invoke chezmoi_modify_manager. It should be one of:
Use this if chezmoi_modify_manager is installed in PATH (recommended):
#!/usr/bin/env chezmoi_modify_manager
Use this if you keep chezmoi_modify_manager in your chezmoi source directory:
#!{{ .chezmoi.sourceDir }}/.utils/chezmoi_modify_manager-{{ .chezmoi.os }}-{{ .chezmoi.arch }}
In addition, the way to specify the source file has changed. The line to specify the source file would now typically look like:
source "{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini"
Finally, you need to convert the actual ignores and transforms themselves:
-ik key value -> ignore "key" "value"
-is section-name -> ignore section "section-name"
-ikr key-re value-re -> ignore regex "key-re" "value-re"
# Note change of argument order for transforms, the transform name
# now comes after the match.
-tk transform_name key value '{ "arg1": "value1", "arg2": "value2" }'
-> transform "key" "value" transform-name arg1="value1" arg2="value2"
-tkr transform_name key value '{ "arg1": "value1", "arg2": "value2" }'
-> transform regex "key" "value" transform-name arg1="value1" arg2="value2"
Transforms
Transform arguments have changed. Before they were a JSON object, now they
are a series of key="value"
.
Apart from that, transform names have changed:
- kde_shortcut -> kde-shortcut
- unsorted_list -> unsorted-list
Finally, the argument name has changed for keyring: username
is now just user
.
Keyring
As stated in the previous section, the argument names have changed.
In addition, because the backend for talking to the platform secret store is different, there can be other incompatibilities. Known ones include:
- On Linux, KDE KWallet is no longer supported. Only secret stores over DBus SecretService are supported. This means it will likely end up using GNOME's secret store (Seahorse) instead. See the example for konversationrc for how to add the password, if you need to migrate.
Other platforms are untested (since I don't have any of those), but I welcome any feedback to improve this documentation.
Migration from version 2.x to 3.x
Migrating from hook scripts
In 2023 hook scripts were deprecated and then removed in early 2024 in version 3.0. They
are now replaced by the add:remove
and add:hide
directives.
For example, to make sure that a password isn't added back into the source state you might use something like this:
transform "LoginSection" "Password" keyring service="myprogram" user="myuser"
# Make sure the password isn't added back into the config file on re-add
add:hide "LoginSection" "Password"
This would pull the password from the OS keyring, but erase it when doing a re-add.
The add:remove
directive can be used to completely remove the entry instead.
This can be useful together with set
and system specific configuration:
{{ if (.is_work) }}
set "Default Applications" "x-scheme-handler/jetbrains" "jetbrains-toolbox.desktop" separator="="
{{ end }}
# Completely remove the line when adding back (regardless of which computer this is on).
add:remove "Default Applications" "x-scheme-handler/jetbrains"
This example for mimeapps.list
would add an entry only when .is_work is true,
but also make sure that the value isn't added back to the config file and thus
prevents transferring it to other computers by mistake.
NOTE: The
add:hide
andadd:remove
directives are processed as is without going through chezmoi's template engine when re-adding files. This means it won't matter if they are inside an if block, nor can you use template expressions in their arguments.
NOTE:
ignore
directives also result in an implicitadd:remove
. Again, it doesn't matter if it is inside an if block or not currently during adding of files.
Troubleshooting
The first step should be to run chezmoi_modify_manager --doctor
and correct
any issues reported. This will help identify some common issues:
- chezmoi_modify_manager needs to be in
PATH
**/*.src.ini
needs to be ignored in the root.chezmoiignore
file- Old chezmoi and/or using
CHEZMOI_MODIFY_MANAGER_ASSUME_CHEZMOI_VERSION
, see this documentation for more details on when or when not to use this.
Limitations
Alas, no software (apart from perhaps a simple Hello, world!
) is perfect.
Here are some known limitations of chezmoi_modify_manager
:
- When a key exists in the
.src.ini
file but not in the target state it will be added to the end of the relevant section. This is not an issue as the program will usually just resort the file next time it writes out its settings. modify_
scripts bypass the check for "Did the file change in the target state" that chezmoi performs. This is essential for proper operation. However, it also means that you will not be asked about overwriting changes. Always look atchezmoi diff
first! I do have some ideas on how to mitigate this in the future. See also this chezmoi bug for a more detailed discussion on this.
source
: How chezmoi_modify_manager finds the data file
Background
chezmoi_modify_manager needs three inputs to work:
- The modify script with directives (ignores, transforms, etc)
- The state of the config file in your home directory
- The source state of the config file.
The first two are provided by chezmoi, no issues. But as far as chezmoi is concerned, the modify script itself is the source state. As such we need an alternative mechanism.
Problem
The obvious solution would be a path relative to the modify script. However,
chezmoi always copies the modify script to a temporary directory before executing
it, even if the modify script isn't templated. So this doesn't work. (It is however
used internally in the test suite of chezmoi_modify_manager using
source auto-path
, which might be relevant if you are working on the
chezmoi_modify_manager codebase itself.)
Prior to chezmoi 2.46.1, we had to rely on making the modify script a template, as chezmoi didn't expose enough information to us (see this chezmoi issue for more historical details). Basically we can make chezmoi find the source file for us using the following line:
source "{{ .chezmoi.sourceDir }}/{{ .chezmoi.sourceFile | trimSuffix ".tmpl" | replace "modify_" "" }}.src.ini"
Since chezmoi 2.46.1, chezmoi now provides us with two environment variables:
CHEZMOI_SOURCE_DIR
: Path to the source directory rootCHEZMOI_SOURCE_FILE
: Path to our modify script (relative the source directory root)
With these two together we no longer need templating, and the following works:
source auto
What the code does
Since chezmoi_modify_manager 3.1, it will auto-detect the version of chezmoi
(based on executing chezmoi --version
). This is used for:
- The template that
--add
creates to either use the templated source string or the simplersource auto
. - Interpreting the meaning of
--style=auto
(default value for style) to either create a templated modify script or a non-templated modify script.
The main benefit of the simpler source auto
is that if your modify script
doesn't need to be a template for any other reason, it will speed up execution,
as chezmoi no longer needs to run its template engine.
Overriding auto detection
Auto-detection has one downside though: What if you use multiple versions of chezmoi (such as an old version from Debian stable on some server but an up-to-date version on your personal computer). In that case you don't want to use the newer syntax for compatibility reasons.
The workaround is to export an environment
variable CHEZMOI_MODIFY_MANAGER_ASSUME_CHEZMOI_VERSION
set to the oldest
version that you use. E.g:
CHEZMOI_MODIFY_MANAGER_ASSUME_CHEZMOI_VERSION=2.46.0
This could be set in your .bashrc
/.zshrc
/.profile
or similar file (the
details of how to best set environment variables for a particular platform and
shell is out of scope of this documentation).
Actions & directives
This is a high level overview of how chezmoi_modify_manager applies your config.
For the details on specific directives see chezmoi_modify_manager --help-syntax
.
Glossary
- Directive: Things like
source
,ignore
,transform
,set
,add:hide
, etc that you put in your config file. They are documented in the output ofchezmoi_modify_manager --help-syntax
. - Actions: The directives are internally translated into a ruleset of actions.
These are very similar to the directives, but may not correspond 1:1. For example:
set
becomes a special transform internally.source
doesn't enter the actions, it is only used to figure out what file to load.- etc.
Contexts
There are two different "contexts" for evaluating actions:
- Merging: This is the normal algorithm, used during
chezmoi apply
(anddiff
etc) - Filtering: This is using when re-adding an existing file (
chezmoi_modify_manager -a
or-s
).
See Algorithms for details of how these work, in this file we are only concerned with how the directives and rules matching works.
These have separate directive to action translators. Not all directives apply to all contexts. Some examples:
set
is unused when filteringadd:hide
is unused when mergingignore
translates to the same asadd:remove
when filtering.- etc.
Order of action matching
Actions come in three flavours:
- Section matches (always literal matches)
- Literal section+key matches
- Regular expression section+key matches
Not every rule can exist in every variant. For example:
- Merge section matches only support
ignore
andremove
. set
will only ever exist as a literal section+key match- etc.
Chezmoi_modify_manager uses a single regex to match both the section and key.
This is done by constructing a combined string for these, using the 0-byte
(\0
) as a separator. For example a regex directive
ignore regex "Section|OtherSection" "SomePrefix.*"
is compiled down to
(?:Section|OtherSection)\0(?:SomePrefix.*)
. This can be visible if you attempt
to use ^
or $
(don't do that).
The special string <NO_SECTION>
is used to match keys that appear before the
first section (hopefully no one has an ini-file with a section with that name in it).
When matching actions:
- We first check if any section action applies. If so we are done. These are always literal matches.
- Then we check if there is a literal section+key match. If so it applies and we are done.
- Otherwise, we check if any regex action matches. If so we take the first result. This will be the same as first in source order in your config file.
Additionally, chezmoi_modify_manager will warn if there are multiple regex matches
that match. This can be disabled (per file) with a no-warn-multiple-key-matches
directive, in case you want this behaviour.
Algorithms
This documents a high level overview of the algorithms chezmoi_modify_manager uses for merging or filtering INI files.
In general these algorithms are single-pass, processing one line at a time from the input INI file. This makes them quite fast in practice.
The code for these are implemented in the ini-merge crate. The explanation here is intended for users, and as such leaves out a lot of gnarly details around for example: empty sections, sections with only comments in them, etc. If you are interested in that, go read the code.
The actual INI parser is in the ini-roundtrip crate. A custom INI parser is used to ensure that writing it back out doesn't change formatting.
Filtering
This is used when re-adding an existing file (chezmoi_modify_manager -a
or -s
).
This is the simpler of the two algorithms.
Relevant directives from your config for this algorithm:
add:hide
(replaces the value withHIDDEN
when re-adding)add:remove
(removes the line when re-adding)ignore
(removes the line when re-adding)
(The reason there are two directives with the same effect is that they do different things when merging.)
When a user passes -s
or -a
a bunch of things happen:
- We figure out if the file is already managed or not. Depending on
-a
or-s
we will then do different things. This is not the focus of this page though. - Assuming we decided that we should add the file and manage it using
chezmoi_modify_manager
(instead of plainchezmoi
), and that the file was already managed by us before, we then need to filter:- Load the ruleset that the user wrote into an Actions structure. Currently, this does not take chezmoi templates into account (though this might change).
- For each line in the file being filtered:
- If it is a new section header, check section actions to determine if it should be removed entirely, otherwise keep it.
- If it is a comment or blank line keep it (unless the entire section is being removed)
- If it is a key, check actions to determine if it should be hidden, removed or kept.
Note evaluation order of actions documented in Actions, section matches take priority, then literal matches, then regex matches (in order).
Merging
This is used for normal chezmoi apply
(and chezmoi diff
etc). This is a more
complicated case: there are now three files involved.
Relevant directives from your config for this algorithm:
ignore
(keeps the system state and ignores whatever is in the source state)set
(sets to a specific key and value)remove
(entirely removes the match)transform
(applies a custom transform to the match, see--help-transforms
, custom semantics apply to each)
- Load the ruleset that the user wrote into an Actions structure. Chezmoi has already processed any templates for us.
- Load the
.src.ini
file into a fast data structure for looking things up in it. - For each line in the system state (as provided by chezmoi on stdin):
- If it is a comment or blank line, keep it (unless it is in a section that we are not outputting).
- If it is a section header, check:
- If the entire section is ignored, keep it as is from the system state.
- If the section is being removed by
remove
, remove it. - If the section exists in the .src.ini, keep it.
- If the section doesn't exist in the .src.ini, remove it.
- (There is also some additional logic to deal with entirely empty sections etc, so we don't actually emit the section on stdout until we are sure later on, there is a concept of "pending lines" to implement that.)
- If it is a key, find the first action that applies
if any. Then:
- If no action applies, take the value from the
.src.ini
file. - If no action applies and the line is not in the
.src.ini
file, remove the line. - If the action is to
ignore
, leave the system value as is. - If the action is to
remove
, remove it. - If the action is to
set
, set it. - If a transform applies, apply it (see each transform for more details).
- If no action applies, take the value from the
- Before we start a new section, check if there are any lines in the
.src.ini
that didn't exist in the system state (or any suchset
directives), if so emit them. - Before the end of the file, check for entire sections (or
set
directives in such sections) in the.src.ini
that didn't exist in the system state, if so emit them.
The newly emitted keys or sections from the last two bullet points will generally be weirdly formatted. The assumption is the program that owns this file will reformat it on next use.
Packaging for Linux distros etc
Do you want to package chezmoi_modify_manager in your favourite package manager? Awesome! Here are some helpful notes for you.
- Please tell me about your package (file a github issue): I will link to any suitable package from the README (with a written caveat that I don't maintain them and thus cannot guarantee that they are up-to-date or safe to use). I maintain the AUR package myself as of writing this (thus there is no disclaimer there).
- If you need some inspiration for how to build and install properly, you might want to take a look at my AUR PKGBUILD. Arch Linux uses a relatively simply format (bash scripts), so it should be easy to decode and adapt.
How to build
- When building, please export the environment variable
CHEZMOI_MODIFY_MANAGER_BUILDER
. This will be reported inchezmoi_modify_manager --doctor
, and can be very helpful in bug reports. If you don't set this,--doctor
will report it as a warning (assuming that it is an unknown local build on someone's computer).- Please set it to a short and truthful value (such as "debian", "homebrew", "aur" etc) that identifies the package ecosystem. If your package build the latest and greatest git version, please add a suffix indicating so (e.g. "aur-git")
- Especially don't claim to be "github-ci" or "github-release" as those are used for the official binary releases. That would be Not Cool!
- Build a release build (
--release
). Rust is really quite slow in debug builds, but very speedy in release builds. The difference between debug and release builds is much larger than for C/C++. - Build with
--locked
: This ensures that the versions of dependencies are exactly the same as upstream. Otherwise, you might get newer supposedly compatible versions. Those may or may not work. - Build with the stable Rust toolchain: Nightly or beta is not needed and is just asking for potential issues.
- You likely want to exclude the built-in self updater (that downloads from
Github releases), as your package manager should be used instead. This is easy:
pass
--no-default-features --features=keyring
to cargo build. This will also avoid vendoring C dependencies (in particularlibdbus
) and instead link them dynamically. If you don't want that, add thevendored
feature as well (i.e.--features=keyring,vendored
).
And so we arrive at the final (reliable regardless of what environment the user might have) build command:
export CHEZMOI_MODIFY_MANAGER_BUILDER="<your package ecosystem name>"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --locked --release --no-default-features --features=keyring
If you need to download dependencies first (as is best practise for some build systems) this gets split into two phases:
# Download deps
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
# Build
export CHEZMOI_MODIFY_MANAGER_BUILDER=aur
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --frozen --release --no-default-features --features=keyring
Note the change from --locked
to --frozen
in the second command here.
What to install
You should of course install the binary itself chezmoi_modify_manager
. However,
you might also want to install shell completion files relevant to your platform.
These can be generated by executing the built binary with
--bpaf-complete-style-<name of shell>
. Here is the code from the AUR PKGBUILD
to do the entire install:
local _cmd_name="target/release/${pkgname}"
install -Dm0755 -t "$pkgdir/usr/bin/" "$_cmd_name"
mkdir -p "$pkgdir/usr/share/bash-completion/completions/"
mkdir -p "$pkgdir/usr/share/zsh/site-functions/"
mkdir -p "$pkgdir/usr/share/fish/vendor_completions.d/"
"$_cmd_name" --bpaf-complete-style-zsh > "$pkgdir/usr/share/zsh/site-functions/_$pkgname"
"$_cmd_name" --bpaf-complete-style-bash > "$pkgdir/usr/share/bash-completion/completions/$pkgname"
"$_cmd_name" --bpaf-complete-style-fish > "$pkgdir/usr/share/fish/vendor_completions.d/${pkgname}.fish"
# No support to install distro completions in elvish.
# See https://github.com/elves/elvish/issues/1739
#"$_cmd_name" --bpaf-complete-style-elvish
For more info on supported shells, see the bpaf documentation, which is the library used by chezmoi_modify_manager to handle command line parsing.
Design decisions
This file documents motives some design decisions.
Why did you implement a custom INI parser?
I ended up writing my own INI parser for rust:
ini-roundtrip. This had to
be done because standard INI parsers don't support preserving the
formatting. This is not acceptable when trying to minimise the diff. We
want to not change the formatting applied by the program that writes the
settings file. For example KDE writes key=value
while PrusaSlicer writes
key = value
.
It also does minimal parsing, meaning it can handle weird non-standard syntax
such as [Colors:Header][Inactive]
(a real example from kdeglobals
).
Why Rust?
This code used to be written in Python, but each invocation of the command would take on the order of 95 ms. Per managed file. As I was getting up to around 20 managed INI files, this started to add up. The rewrite in Rust takes (on the same computer) 2 ms. This is a 46x speedup. On another (faster) computer I got a 63x speedup (54 ms vs 0.9 ms).
Fast path
The most time critical operation is to compute the new system state when chezmoi
invokes us. This is the "fast path" in the code. All other operations such as
--add
, --update
etc are less important from a performance perspective. This
should be kept in mind when adding new features.