Posted on August 11, 2022 by Adrian Wyssmann ‐ 7 min read
Today, managing your dotfiles should be easy, so that you quickly can setup your environment and have it working as you would expect to. It all starts with a git-repo - with the benefit that you can share your files to others.
Up to now, I had a very simple approach: Checkout files form a git-repo and create symlinks of the managed files. For the creation of the symlinks, I used stow - a manager for symlinks. I organized the files in a way, so that I have different “profiles” so I can have one for my personal environment and one for work. This script took care of the setup:
#!/usr/bin/env bash
# read the option and store in the variable, $option
TARGETDIR=~/
DEFAULTPROFILE=personal
RESTOW=
ADOPT=
# Function: Print a help message.
usage() {
echo "Usage: $0 [ -p PRFOILE ] -R PACKAGNAME|all" 1>&2
}
# Function: Exit with error.
exit_abnormal() {
usage
exit 1
}
while getopts "aRDp:" option; do
case ${option} in
a )
echo "do an 'adopt'"
ADOPT="--adopt --override='.*'"
;;
R )
echo "do a 'restow' i.e. stow -D followed by stow -S"
RESTOW="-R"
;;
D )
echo "delete i.e. stow -D followed by stow -S"
DELETE="-R"
;;
p )
PROFILE=${OPTARG}
echo "profile '$PROFILE' selected\n"c
;;
\? )
exit_abnormal
;;
*)
exit_abnormal
;;
esac
done
if [ ! $PROFILE ]; then
if [ $DEFAULTPROFILE ]; then
echo "using default profile '$DEFAULTPROFILE'"
PROFILE=$DEFAULTPROFILE
else
echo "-p PROFILE was not specified"
exit_abnormal
fi
fi
SOURCE=$(pwd)/$PROFILE
if [ ! -d "$SOURCE" ]; then
echo "Invalid profile '$PROFILE' (path '$SOURCE' missing)"
exit_abnormal
fi
pushd $SOURCE
shift $(($OPTIND - 1))
if [ ! $1 ]
then
echo "no packages specified. syou can use 'all' if you want to install all from the profile or use one of these:"
echo $(ls)
exit_abnormal
else
PACKAGE=$1
if [ $PACKAGE == "all" ]
then
echo "Install all available packages"
for filename in $(find . -maxdepth 1 -mindepth 1 -type d -printf '%f\n'); do
echo stow $RESTOW $DELETE $ADOPT $filename -t $TARGETDIR
stow $RESTOW $DELETE $ADOPT $filename -t $TARGETDIR
done
elif [ -d "./$PACKAGE" ]
then
echo stow $RESTOW $DELETE $ADOPT $PACKAGE -t $TARGETDIR
stow $RESTOW $DELETE $ADOPT $PACKAGE -t $TARGETDIR
else
echo "package '$PACKAGE' missing"
fi
fi
popdThe approach is simple but has some drawbacks
user.mail and user.name in .gitconfigIt worked, but I never was very happy until I stumbled upon chezmoi which sounds very promising, what it offers and how it compares to other tools out there. Most importantly for me
So I decided to move from my original approach to chezmoi. The result can be found in dotfiles. I won’t go into details about how it works, as the official docu is very good and informative. But I want to describe what I did, and show how I solved some “problems” I consider important to me.
I decided to use a new git-repo, as the structure of
chezmoi is different. So I ran a
chezmoi init without specify a repo.
chezmoi created an empty git repo under ~/.local/share/chezmoi. As mentioned above, my approach relied on symlinks. So in order to [add] the files to
chezmoi, using the --follow flag - e.g. chezmoi add ~/.zshrc --follow added the actual file as ~/.local/share/chezmoi/dot_zshrc. The content in ~/.local/share/chezmoi represents the state, whereas the naming of the files uses so called
source state attributes.
I started to go trough all my dotfiles until I stumbled on .muttrc which contains my mail information as well as passwords for the smtp access, I have two choices
I decided to encrypt the whole file for now.
While chezmoi does not have any requirements and installation is pretty easy, if you want to have encryption, you need either a gpg key or a passphrase. I decided to use a passphrase rather than a gpg-key, which skips the manual distribution of the gpg-key - cause as far as I have seen you have to use the same key on all machines. As I am lazy, I don’t want to enter the passphrase for each operation. Luckily the documentation provides an easy way to create the chezmoi.toml config file upon initialization:
If a file called
.chezmoi.$FORMAT.tmplexists thenchezmoi initwill use it to create an initial
Hence, I created the following .chezmoi.toml.tmpl which not only stores then passphrase, but also other machine specific data:
{{ $passphrase := promptStringOnce . "passphrase" "passphrase" -}}
{{ $username := promptStringOnce . "username" "username" -}}
{{ $email := promptStringOnce . "email" "email" -}}
{{ $signkey := promptStringOnce . "signkey" "signkey" -}}
encryption = "gpg"
[data]
passphrase = {{ $passphrase | quote }}
email = {{ $email | quote }}
username = {{ $username | quote }}
signkey = {{ $signkey | quote }}
[gpg]
symmetric = true
args = ["--batch", "--passphrase", {{ $passphrase | quote }}, "--no-symkey-cache" ]While passphrase is used in the same config file, email, username and signkey are used to populate the .gitconfig file, using [templating][templates]-mechanism. So there is a file called dot_gitconfig.tmpl which contains the generic git stuff (aliases, etc) which I use on all machines, but with machine-specific instructions to add the variables from above:
...
[user]
email = {{ .email | quote }}
username = {{ .username | quote }}
{{- if eq .chezmoi.hostname "clawfinger" }}
signkey = {{ .signkey | quote }}
[commit]
gpgsign = true
{{- end }}As the encryption is setup now, I can easilly add ~/.muttrc encrypted by running chezmoi add --encrypt ~/.muttrc. which will add encrypted_dot_muttrc.asc to the state:
-----BEGIN PGP MESSAGE-----
jA0EBwMCJ/rDD1pg2Vb00sDXAY0QuCC7HOuNedu2bhKz1wuilfT/5ikyHh8rBMTb
whKvmo5EdOU7rPCKgXpXSEbgaop3mfJrvaPk8aNy4l2Ao9FJGG7Rs4Ctn6PgbuWY
diD5Jr8+pB4YS8dO5/vg0vgApzMALhrSWa0BbMuh4Nm8OoD1HROKketL2+kexIP4
R2pfTBWXPmeHRfeIXvyb0V0WPjhGfm8YoFa+bx/g3Npe7roNIOM/qBLrndY3c/j5
+8ItcQlT7lD1wcHS784nyg0xwz0gorGBTyItwDCJUBbHdMFe6oZs9tTFgB/UDD6M
puSNHh4Ippe0Btfrbz2RgzVBGISc6Vr8tEkXHzzzRg6hrOjMXW6z6vPb+y2mquF6
J/RctFb9hMwfCT7sxhscOtPmJjWgQ0HPddt2nEpr6bc27hOd0QUWtyJaETgGI/Ye
LOD0Qp9W6SFQP0o2M9WtbjHEcE7qJMAWyXYeKkqI8RA4ln5dZbUJnibQk3gULPuj
N921J8UYcewwXy664763Mh/P2ePH1s2zTEpjhw/luF9AJxcliBcBolU=
=pO9e
-----END PGP MESSAGE-----I have an additional repo with shell-scripts, which contains two folders:
The folders shall be mapped as follows:
scripts to ~ /.local/binnautilus to ~/.local/share/nautilus/As
chezmoi allows
include files from elsewhere, I decided to use this feature. First I added .chezmoiexternal.toml with the following content:
[ ".local/scripts"]
type = "git-repo"
url = "https://gitlab.com/papanito/shell-scripts.git"
exact = true
stripComponents = 2
refreshPeriod = "168h"This checks out the repo to ~/.local/scripts. But how shall I map the subfolders as expected? Well also here
chezmoi helps, as it offers a feature, that
use scripts to perform actions. So I created a file run_once_nautilus_scripts.sh.tmpl which takes care of creating the necessary symlinks:
{{ if eq .chezmoi.os "linux" -}}
#!/bin/sh
stow nautilus -S -d ~/.local/scripts -t ~/.local/share/nautilus
stow scripts -S -d ~/.local/scripts -t ~/.local/bin
{{ else if eq .chezmoi.os "darwin" -}}
#!/bin/sh
stow nautilus -S -d ~/.local/scripts -t ~/.local/share/nautilus
stow scripts -S -d ~/.local/scripts -t ~/.local/bin
{{ end -}}As described here there are some simple commands to execute:
Pull the changes from your repo and apply them in a single command
chezmoi updateIf you want to see the changes before applying
chezmoi git pull -- --rebase && chezmoi diffApply changes
chezmoi applyIf you want to install chezmoi and your dotfiles on a new machine with a single command, run this:
sh -c "$(curl -fsLS https://chezmoi.io/get)" -- init --apply $GITHUB_USERNAMEFor setting up transitory environments (e.g. short-lived Linux containers) you can install chezmoi, install your dotfiles, and then remove all traces of chezmoi, including the source directory and chezmoi’s configuration directory
sh -c "$(curl -fsLS https://chezmoi.io/get)" -- init --one-shot $GITHUB_USERNAMEchezmoi is a undoubtedly very cool to manage your dotfiles and as you can see very flexible. It’s also very easy to use. Once I am back at work, I will also migrate my work-files to it, so I have a single source of truth.
Btw. if you are using navi, you can find here my cheatsheet for chezmoi.