Managing dotfiles with GNU Stow
Many developers manage their user-specific application configuration – also known as dotfiles – in a version control system such as git. This allows for keeping track of changes and synchronizing the dotfiles across different machines. Searching on github, you’ll find thousands of dotfile repositories.
As your dotfiles are sprinkled all over your home directory, managing them in
a single repository is not trivial, i.e. how do you make sure that your
.bashrc
, .tmux.conf
, etc. that life in your dotfile repository appear in
the proper places in your home directory? The most common solution is to use
symlinks so that the .tmux.conf
in your home directory is just a symlink
pointing to the appropriate file in your dotfile repository:
$ ls -l ~/.tmux.conf
lrwxrwxrwx 1 venthur venthur 34 18. Dez 22:53 /home/venthur/.tmux.conf -> git/dotfiles/tmux/.tmux.conf
This leads immediately to another problem: how do you manage the symlinks? For the longest time I just manually maintained the symlinks on the various machines, but this approach does not scale well with the number of dotfiles and machines you’re using this repository on. Often, people write their own shell scripts that help them with the maintenance of the symlinks, but at least the solutions I’ve seen so far did not convince me.
Last year I stumbled upon GNU Stow, an unpretentious little tool that does not reveal at first sight how useful it would be for the job. The description on the website says:
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.
Right.
How does it work?
In stow
’s terminology, a package is a set of files and directories that
need to be “installed” in a particular directory structure. The target
directory is the root of the tree in which the package appear to be
installed.
When you “stow” a package, stow
creates symlinks in the target directory
that point into the package.
Let’s say I have my dotfiles repository in ~/git/dotfiles/
. Within this
repository, I have a tmux
package, containing the .tmux.conf
dotfile:
$ pwd
/home/venthur/git/dotfiles
$ find tmux
tmux # the package
tmux/.tmux.conf # the dotfile
The target directory is my home directory, as this is where the symlinks
need to be created. I can now stow the tmux
package into the target
directory like so:
$ stow --target=/home/venthur tmux
and stow
will create the appropriate symlinks to the contents of the package
into the target directory:
$ ls -l ~/.tmux.conf
lrwxrwxrwx 1 venthur venthur 34 2. Jun 2021 /home/venthur/.tmux.conf -> git/dotfiles/tmux/.tmux.conf
Note that the name of the package (i.e. the name of the directory) does not
matter as stow
points the symlinks into the package, so you can choose
it freely. I usually use the name of the program that the configuration
belongs to as the package name.
Your package can also contain several files or even a complex directory
structure. Let’s look at the configuration for neovim, which lives below
~/.config/nvim/
:
$ pwd
/home/venthur/git/dotfiles
$ find neovim
neovim
neovim/.config
neovim/.config/nvim
neovim/.config/nvim/init.vim
$ stow --target=/home/venthur neovim
$ ls -l ~/.config/nvim
lrwxrwxrwx 1 venthur venthur 41 2. Jun 2021 /home/venthur/.config/nvim -> ../git/dotfiles/neovim/.config/nvim
At this point we should mention that the target directory for my dotfiles will always be my home directory, so the contents of the packages are either the configuration files or the directory structure as they live in my home directory.
Deleting a package from the parent directory
You can also remove (unstow) a package from the target directory again, using
the --delete
parameter:
$ ls -l ~/.tmux.conf
lrwxrwxrwx 1 venthur venthur 34 18. Dez 22:53 /home/venthur/.tmux.conf -> git/dotfiles/tmux/.tmux.conf
$ stow --target=/home/venthur --delete tmux/
$ ls -l ~/.tmux.conf
ls: cannot access '/home/venthur/.tmux.conf': No such file or directory
Stowing several packages at once
Since your dotfile repository will likely contain more than one package, it
makes sense to combine the individual stow
commands into one, so instead of
stowing everything individually,
$ stow --target=/home/venthur tmux
$ stow --target=/home/venthur vim
$ stow --target=/home/venthur neovim
you can stow everything at once:
$ stow --target=/home/venthur */
Note that I use */
instead of *
to match all directories (i.e. packages),
since my dotfiles repository also contains a README.md
and a makefile
.
Putting it all together
My dotfiles repository contains a makefile
that allows me to
create/update or delete all symlinks at once:
all:
stow --verbose --target=$$HOME --restow */
delete:
stow --verbose --target=$$HOME --delete */
The --restow
parameter tells stow
to unstow the packages first before
stowing them again, which is useful for pruning obsolete symlinks from the
target directory.
Et voilà! Whenever I make a change in my dotfiles repository that involves creating or deleting a dotfile (or a package), I simply call:
$ make
and everything is updated. To delete all dotfile-related symlinks from this machine, I simply run:
$ make delete