Hey there, and welcome to my second blog post! It's been almost half a year since my last update, and a lot has happened. I'm now 18 and almost finished with my Abitur, which explains the big gap between posts.
Today, I'll be covering the topic of dotfiles management. For those who aren't familiar, dotfiles are a set of configuration files for a developer's favorite tools. These files are typically hidden in your system, prefixed with a .
, and you can see them if you enable the "show hidden files" option in your file explorer.
The issue with dotfiles is that they are scattered across different directories because each tool has its own preferred location for configuration files. This is where a dotfiles management system comes in handy. There are various approaches to managing these files efficiently, and I'll be discussing some of them today.
For those not interested in reading everything from the start, just skip to the chezmoi section of this article.
For those who can’t decide between dotfiles managers, read this article about dotfiles management.
Bare Git Repo
The most popular article to this topic is linked here.
I won't dive too deep into this approach because the idea behind using a bare-bones Git system for managing dotfiles is quite simple. The main advantage is that it's lightweight. However, as your configuration files grow in number and complexity, managing them becomes more challenging. You'll end up with a massive .gitignore
file, and you'll need to keep track of your entire $HOME
directory, which can be quite cumbersome. Let's move on to a more advanced method.
GNU Stow
GNU Stow is a symlink farm manager which takes distinct packages of software and/or data located in separate directories on the filesystem, and makes them appear to be installed in the same place (from the GNU Stow documentation).
It basically simplifies managing software and data by creating symbolic links from separate directories to a target directory, in this case, the $HOME
directory. Imagine having a .dotfiles
directory structured like this:
$HOME/.dotfiles
├── .config
│ ├── nvim
│ │ └── init.lua
├── .git
├── .tmux.conf
├── .stow-local-ignore
└── README.md
Running stow .
sets everything up effortlessly. Stow arranges symbolic links for files and directories, providing a centralized space for configuring and organizing tools while keeping them functional. Essentially, $HOME/.dotfiles
(or %USERPROFILE%\.dotfiles
) mirrors the $HOME
(%USERPROFILE%
) directory, enhancing clarity and organization.
One significant advantage is its lightweight and straightforward setup process. Unwanted files, like a README.md
, can be included in .stow-local-ignore
. However, the reliance on symbolic links poses some challenges. When target files are deleted, broken symlinks remain. Additionally, managing files across different operating systems can lead to compatibility issues and little performance overhead.
For more insights into symlink challenges, check out this article.
While these drawbacks exist, GNU Stow remains a popular choice and a straightforward recommendation for managing non-cross-platform dotfiles.
There's a newer option called dotbot
(written in Python), addressing concerns like cross-platform compatibility. It resides in your GitHub repository as a Git Module. If you're okay with symlinks and seek a modern, cross-platform compatible alternative to GNU Stow, dotbot is worth trying.
Next, we'll delve into tools that sidestep the symlink issue.
Other Dotfiles Managers
yadm
There is yadm. Most of these operations will look like Git commands; because they are. yadm wraps Git, allowing it to perform all of Git’s operations. The difference is your $HOME
directory becomes the working directory, and you can run the commands from any directory (from the yadm Docs). It’s cross-platform compatible (though I couldn't find Windows support), uses actual files instead of symlinks, and still uses a single point of truth. Additionally, it supports templates, letting you customize your environment based on globals or other external factors like the operating system or environment variables. It also supports encryption, which is important if you want to add your SSH keys or other confidential files. To be honest, this seems well thought out and is likely enough for many people. Personally, I would choose it over GNU Stow, but for me, it didn’t click. It’s very reliant on the command line, and to be honest, I prefer relying on configuration files that the CLI monitors.
Ansible
I should mention Ansible
because it can handle anything yadm can't. Although it's not specifically a dotfiles management tool, it's a powerful automation tool. Ansible is a good choice if you know what you're doing. It's cross-platform compatible, and you can run custom scripts and add global variables. Ansible works with roles and tasks, which you can use to set up a new environment. Plus, Ansible Galaxy offers a wide range of plugins. However, I found it too complex for my needs. You might think that chezmoi (which I'll discuss next and is my ultimate choice) is also too much, but I use many of its features. Ansible felt more powerful than what I needed for managing dotfiles.
Home Manager
There is also Home Manager, a system built for managing NixOS user environments using the Nix package manager and the Nixpkgs libraries (from the Home Manager Docs). I personally don’t use NixOS, but if you are, this could be a more suitable choice for you.
Check out this article to discover more dotfiles managers. If you need a comparison between popular dotfiles managers, check out this comparison table.
chezmoi
So far we are searching for the following features:
-
cross-platform compatibility (including Windows)
-
no symlinks
-
config files & templates with data
-
encrypted files
chezmoi provides many features beyond symlinking or using a bare git repo including: templates (to handle small differences between machines), password manager support (to store your secrets securely), importing files from archives (great for shell and editor plugins), full file encryption (using gpg or age), and running scripts (to handle everything else) (from the chezmoi Docs).
By default, your dotfiles repo is stored in $HOME/.local/share/chezmoi
(on Windows: %USERPROFILE%\.local\share\chezmoi
). I personally like it to be in $HOME/.dotfiles
(on Windows: %USERPROFILE%\.dotfiles
), but more to that later.
Example
Let's go through an example, which you can also find in the quick start guide:
After initializing chezmoi with chezmoi init
(which creates the default source directory), you can start adding files with chezmoi add
.
Imagine we have a ~/.bashrc
file. You can add it to your dotfiles repo by running:
This creates a copy of the file in $HOME/.local/share/chezmoi
, but it converts .bashrc
to dot_bashrc
. Files starting with .
in $HOME/.local/share/chezmoi
are ignored, which is useful.
To learn more about this, check out this page.
To edit your Bash configuration, instead of editing ~/.bashrc
directly, use:
This opens $EDITOR
to edit $HOME/.local/share/chezmoi/dot_bashrc
. If you use Vim, Neovim, or Nano, this works well. If you use VSCode, you might find it easier to run:
This changes the directory to $HOME/.local/share/chezmoi
so you can run code .
.
Personally, I run chezmoi cd
and then nvim
.
To see a diff of your changes, use:
Or, if you're in $HOME/.dotfiles
already, you can use lazygit
for a git diff.
To apply local changes, run:
This updates ~/.bashrc
with changes from $HOME/.local/share/chezmoi/dot_bashrc
.
There are more filename modifiers like exact_
, which ensures all files in a directory are managed by chezmoi, deleting unmanaged files.
For more details, check out this list.
I often don't use chezmoi add
because the metadata filenames aren't always needed. The main ones I use are dot_
and exact_
. After making changes, commit them with the normal git CLI. To get the latest repo changes, run:
So, chezmoi apply
is for local changes, and chezmoi update
is for repo changes.
To bootstrap your dotfiles with one command, run:
chezmoi init --apply https://github.com/$GITHUB_USERNAME/dotfiles.git
Or, if your repo is named dotfiles
:
chezmoi init --apply $GITHUB_USERNAME
If someone else wants to use your repo and doesn't have chezmoi, they can run:
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply $GITHUB_USERNAME
Templates
Templates allow you to change the contents of a file based on the environment. For example, you can use the hostname of the machine to create different configurations on different machines (from the chezmoi Docs).
The templating syntax is from Go, extended with the sprig
library. chezmoi detects a template when either:
-
the
.tmpl
file extension is used
-
or the template is located in
.chezmoitemplates
or any subdirectory of it.
Text outside actions is copied literally. Let’s look at an example from my dotfiles:
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
{{- if eq .chezmoi.os "darwin" }}
eval "$(/opt/homebrew/bin/brew shellenv)"
{{- end }}
alias vim="nvim"
alias vi="nvim"
alias v="nvim"
alias c="clear"
This is part of my Zsh configuration.
The dashes at the beginning of if
and end
remove any trailing whitespace before the statement, just for formatting reasons.
.chezmoi.os
is one of many variables available in the template. .chezmoi.os
shows the current device’s operating system. darwin
means macOS.
The eq
function is used because there is no equal operator like ==
. It compares the two arguments that follow it.
Other functions include:
-
eq
(equal)
-
not
(not equal)
-
and
(both conditions must be true)
-
or
(one condition must be true)
If the operating system is macOS, it adds everything enclosed within the if
statement. Here’s how it would look:
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
alias vim="nvim"
alias vi="nvim"
alias v="nvim"
alias c="clear"
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
eval "$(/opt/homebrew/bin/brew shellenv)"
alias vim="nvim"
alias vi="nvim"
alias v="nvim"
alias c="clear"
Template actions still work inside quotes. To prevent LSP diagnostic errors, enclose the template action in quotes. Template actions also work in comment lines:
eval "$(/opt/homebrew/bin/brew shellenv)"
This helps avoid IDE complaints. I often use this in my .chezmoiscripts
, which I’ll cover next.
To see available data variables, run:
To add custom data, either:
-
Add an entry under
data
in the .chezmoi.yaml|toml|json|jsonc
.
data:
font: "Monaspice Neon Var"
-
Add an entry in
.chezmoidata.yaml|toml|json|jsonc
.
-
Add a whole directory called
.chezmoidata
and add any files in it with the extension yaml
, toml
, json
, or jsonc
. All files in it are interpreted as template data.
.chezmoidata
├── apps.yml
├── arch.yml
├── macos.yml
├── packages.yml
└── windows.yml
Next are shared templates. These are useful for similar but not identical code across different operating systems.
Create a .chezmoitemplates
file, and any file in it will be interpreted as a template without needing the .tmpl
extension.
SOME_IMPORTANT_VARIABLE="TEST"
FONT_SIZE={{ . }}
To use a template, do:
{{- template "test" 14 -}}
You can use it everywhere. For example, if you need shared logic in both ~/.bashrc
and ~/.zshrc
, this is possible with shared templates.
To pass no arguments into shared templates, use:
{{- template "test" . -}}
For multiple arguments:
FONT="{{ .font }}"
FONT_SIZE={{ .fontsize }}
Do:
{{- template "test" dict "font" "Monospace Neon Var" "fontsize" 14 -}}
The dict
function creates a map of key-value pairs.
Shared templates are also helpful if you want the same config file in different locations depending on the operating system. For example, the VSCode configuration:
{
"editor.lineNumbers": "relative",
...
}
{{- template "Code/User/settings.json" . -}}
{{- template "Code/User/settings.json" . -}}
{{- template "Code/User/settings.json" . -}}
This way, I have the same VSCode configuration across all my machines.
Scripts
chezmoi supports scripts that run when you execute chezmoi apply
. These scripts can run every time, only when their contents change, or only if they haven't been run before.
All script files are prefixed with run_
and one or two additional modifiers:
-
after_
- Run script after updating the destination
-
before_
- Run script before updating the destination
-
once_
- Run the script only if it hasn't been run before
-
onchange_
- Run the script only if the filename hasn't been run before
.chezmoiscripts
├── arch
│ ├── run_after_01-enable-services.sh.tmpl
│ ├── run_after_02-configure-gnome.sh.tmpl
│ ├── run_after_03-add-ags-types.sh.tmpl
│ ├── run_after_04-configure-brightness.sh.tmpl
│ ├── run_after_05-configure-account.sh.tmpl
│ ├── run_before_01-configure-locales.sh.tmpl
│ ├── run_before_02-configure-pacman.sh.tmpl
│ ├── run_before_03-configure-reflector.sh.tmpl
│ ├── run_before_04-install-paru.sh.tmpl
│ ├── run_before_05-chsh.sh.tmpl
│ ├── run_onchange_after_01-configure-grub.sh.tmpl
│ ├── run_onchange_after_02-configure-sddm.sh.tmpl
│ └── run_onchange_after_03-configure-hyprshade.sh.tmpl
├── mac
│ ├── run_before_01-ensure-brew-installed.sh.tmpl
│ ├── run_onchange_02-set-defaults.sh.tmpl
│ └── run_onchange_03-brew-bundle.sh.tmpl
├── unix
│ ├── run_after_01-update-neovim.sh.tmpl
│ ├── run_onchange_01-install-packages.sh.tmpl
│ ├── run_onchange_02-install-apps.sh.tmpl
│ └── run_onchange_03-display-manual-installations.sh.tmpl
└── windows
├── run_after_01-symlink-nvim.ps1.tmpl
├── run_after_02-update-neovim.ps1.tmpl
├── run_onchange_01-install-packages.ps1.tmpl
├── run_onchange_02-install-apps.ps1.tmpl
└── run_onchange_03-display-manual-installations.ps1.tmpl
You might notice that these scripts support templates, which is a big advantage. You can use the same data for templates and scripts. This feature sets chezmoi apart.
My run_onchange
scripts make sure I have installed all packages and dependencies listed in .chezmoidata
. When you add a new dependency, run_onchange_01-install-packages
will run again because the file has changed.
Scripts are automatically excluded from the target directory.
Ignore Files
Not everything can be included for every operating system, so you can create an ignore file called .chezmoiignore
. This file also supports templates, and you don’t need to add the .tmpl
extension.
Here's an example:
{{ if ne .chezmoi.os "windows" }}
.chezmoiscripts/_windows/**
.chezmoiscripts/windows/**
AppData
Documents
.wslconfig
{{ end }}
{{ if ne .osid "linux-arch" }}
.chezmoiscripts/arch/**
Pictures
.config/ags
.config/bat
.config/Code
.config/hypr
.config/lazygit
.config/paru
.config/project-manager
.hushlogin
.zprofile
{{ end }}
{{ if ne .chezmoi.os "darwin" }}
.chezmoiscripts/mac/**
Library
{{ end }}
{{ if eq .chezmoi.os "windows" }}
.chezmoiscripts/_unix/**
.chezmoiscripts/unix/**
.config/tmux
.oh-my-zsh
.zshrc
{{ end }}
.config/nvim/templates
.config/ags/bun.lockb
.config/ags/node_modules
.config/ags/package.json
.config/ags/prettier.config.mjs
.config/ags/types
There are separate sections for Windows, Linux, and macOS exclusive folders and files, as well as shared files across UNIX environments. I also exclude scripts, as ps1
files are for Windows and sh
files are for UNIX.
External Dependencies
Imagine you want to clone a repo, but you don't want to manually update it. Instead of running git pull
each time, you can use .chezmoiexternal.yaml|toml|json|jsonc
to keep track of dependencies. This file supports different types of sources like files, archives, and git repos.
For example, if you use Oh My Zsh and don't want to manually upgrade it, you can set it up like this:
".oh-my-zsh":
type: archive
url: "https://github.com/ohmyzsh/ohmyzsh/archive/master.tar.gz"
stripComponents: 1
refreshPeriod: "168h"
exclude:
- "*/.*"
- "*/templates"
- "*/themes"
For template support, add the .tmpl
extension.
Entries are indexed by the target name relative to the directory of the .chezmoiexternal.$FORMAT
file and must include a type and a URL. chezmoi will create any necessary parent directories (from chezmoi Docs).
Check out this reference for more details.
Some Additional Features I Use
-
Use another source directory: Instead of using
$HOME/.local/share/chezmoi
, I use $HOME/.dotfiles
. You can set this in .chezmoi.yaml|toml|json|jsonc
like this:
sourceDir: "/home/<user>/.dotfiles"
sourceDir: "C:/Users/<user>/.dotfiles"
-
Clean repo structure: Use
.chezmoiroot
to relocate the source directory. For example, if you have:
your source directory becomes $HOME/.local/share/chezmoi/home
. This keeps your repo clean, especially if you have a README.md
, LICENSE
, or other project files. Here’s what the root of my repo looks like:
.dotfiles
├── home
├── LICENSE
├── luasnip.yaml
├── neovim.yaml
├── README.md
├── scripts
├── selene.toml
└── stylua.toml
Everything related to dotfiles is in the home
directory, which you can name whatever you want.
Conclusion
I couldn’t cover every topic. There are more dotfiles managers out there, and more features in chezmoi
, such as Password Manager Integration or encrypted files.
But, as always, this has to come to an end. This was my second attempt at a blog post. I’m no expert; I started exploring dotfiles management just over half a year ago (Dec 2023). I wanted to share my opinions on some managers. For those who want something simple, GNU Stow
is surely enough, and yadm
is great if you want additional templating and no symlinks. You can also use Ansible
if you’re familiar with it, or Home Manager
if you use NixOS.
For me, using chezmoi
really leveled up my dotfile management, making it easier than ever to bootstrap a new environment. See you next time in my next blog post. Cheers!