Published on

Tutorial 2: How to manage dotfiles using GNU Stow

Authors
  • avatar
    Name
    Shibi Suriya
    Twitter

Now that you know what dotfiles are, the benefits you can experience by managing them & how to manage them by moving them into a git repository & manually symlinking them to their original location using the ln command, we will be improving our existing setup from tutorial 1 using GNU Stow - a neat tool that automates the process of creating & removing symlinks.

We will first revisit our existing setup, learn what GNU Stow is, its quirks and then integrate it into our existing setup.

Existing setup

We have created a git repo at ~/Desktop/dotfiles using the git init command & moved all of our dotfiles into it. We are then symlinking each dotfile to its original location using the ln command. For example, the bash shell looks for .bashrc (bash's configuration file) at ~ (user's home directory - /user/<username>).

cd ~/Desktop/dotfiles
ls -a
.  ..  .bashrc  keybindings.json
ln -s .bashrc ~/

Since we have multiple dotfiles in our git repository, typing & executing the ln command for each dotfile is annoying! So we have written the ln commands in a shell script - symlink.zsh.

To symlink all of your dotfiles to their original location,

./symlink.zsh

We will be replacing symlink.zsh (a shell script that symlinks each dotfile to its original location) with GNU Stow in this tutorial.

Tutorial

  1. Install GNU Stow,

    • On macOS, using Homebrew,

      brew install stow
      
    • On Debian/Ubuntu based distros, using apt,

      sudo apt install stow
      

Assuming your dotfiles repo is located at ~/Desktop/dotfiles on your computer,

  1. Let's remove symlink.zsh - a shell script that symlinks our dotfiles to their original location. As mentioned earlier we will be replacing symlink.zsh with GNU Stow.

    cd ~/Desktop/dotfiles
    rm symlink.zsh
    
  2. Create Stow packages.

    Let's create Stow packages first, I will explain what Stow packages are later.

    cd ~/Desktop/dotfiles
    ls -a
    .  ..  .bashrc  keybindings.json kitty.conf
    

    We have 3 dotfiles in our repo,

    • .bashrc is bash shell's configuration file, bash looks for .bashrc at ~ - the user's home directory (/Users/<username>).

    • kitty.conf is kitty's (a terminal emulator) configuration file, kitty looks for kitty.conf at ~/.config/kitty/.

    • Visual Studio Code stores its keyboard shortcuts in keybindings.json, it looks for keybindings.json at ~/Library/Application Support/Code/User in macOS and at ~/.config/Code/User in Linux.

    To create stow packages,

    • Create a directory named bash in your dotfiles repo & move .bashrc into it.

    • Create a directory named vscode in your dotfiles repo & move keybindings.json into it.

    • Create a directory named kitty in your dotfiles repo, inside the kitty directory create a directory named .config, inside the .config directory create a directory named kitty & move kitty.conf into it.

    Your dotfiles repo's directory structure should look something like this,

    ~/Desktop/dotfiles
    ❯ tree -a
    .
    ├── bash
    │   └── .bashrc
    ├── kitty
    │   └── .config
    │       └── kitty
    │           └── kitty.conf
    └── vscode
      └── keybindings.json
    
    6 directories, 3 files
    

    The root directory of your dotfiles repo is called the 'Stow directory' (~/Desktop/dotfiles) and its subdirectories bash, vscode & kitty are called 'Stow packages'.

  3. We have 3 stow packages now, the command to stow (to create symlinks) packages to their target location is,

    stow <package-name> -t <target-directory>
    
  4. Let's first stow the packages bash & kitty,

    ~/Desktop/dotfiles
    ❯ tree -a kitty bash
    kitty
    └── .config
        └── kitty
            └── kitty.conf
    bash
    └── .bashrc
    
    4 directories, 2 files
    
    stow bash kitty -t ~
    

    Observe the directory structure of the stow packages & the locations where the symlinks are created,

    • .bashrc will be symlinked to ~/.bashrc.

    • kitty.conf will be symlinked to ~/.config/kitty/kitty.conf.

    GNU Stow uses the directory structure of the stow package & the path supplied to -t to determine where the symlinks needs to be created.

    So, if your stow package's directory structure is,

    ~/Desktop/dotfiles
    ❮ tree  a
    a
    └── b
        └── c
            └── d.txt
    
    3 directories, 1 file
    

    And the path supplied to -t is ~/Desktop, then the file d.txt will be symlinked to ~/Desktop/a/b/c/d.txt.

  5. Stow the vscode package,

    stow vscode -t "~/Library/Application Support/Code/User" # In macOS.
    stow vscode -t "~/.config/Code/User" # In Linux.
    

    As mentioned earlier Visual Studio Code expects keybindings.json at ~/Library/Application Support/Code/User in macOS & at ~/.config/Code/User in Linux.

In this section we will explore some of the bad practices used in most of the popular tutorial on 'How to manage dotfiles using GNU Stow?'.

Users that keep all of their dotfiles in the dotfiles repo's root directory instead of arranging them as Stow packages

You can cd into a stow package & run stow . -t <target-directory> to stow a Stow package instead of mentioning the Stow package's name in the stow command.

So instead of,

cd ~/Desktop/dotfiles # Root directory of the dotfiles git repo.

stow bash -t ~
# Have mentioned the name of the stow
# package in the command itself - bash.

you can use,

cd ~/Desktop/dotfiles/bash # The subdirectory `bash` is a stow package.

stow . -t ~
# GNU Stow assumes that the current working directory is the Stow package.

The point is GNU Stow can't work without Stow packages, most of the popular tutorial on 'How to manage dotfiles using GNU Stow?' available online are confusing because they don't arrange their dotfiles as Stow packages instead they place all of their dotfiles in the root directory of their dotfiles git repo & run the stow . -t ~ command, in this case GNU Stow treats the root directory of the dotfiles repo as the Stow package.

So instead of arranging your dotfiles as Stow packages,

~/Desktop/dotfiles
❯ tree -a kitty bash
kitty
└── .config
    └── kitty
        └── kitty.conf
bash
└── .bashrc

4 directories, 2 files

You can place all of your dotfiles in the root directory of your dotfiles repo,

❯ tree -a
.
├── .bashrc
└── .config
    └── kitty
        └── kitty.conf

3 directories, 2 files

& run the stow . -t ~ command,

.bashrc will get symlinked to ~/.bashrc & kitty.conf will get symlinked to ~/.config/kitty/kitty.conf. Some GNU Stow users prefer to mention the target directory in the .stowrc file (placed at dotfiles repo's root directory) instead of mentioning the target directory using the -t option.

The default 'target directory' is the parent directory of the 'Stow directory'. That is, if you run the command stow . from ~/Desktop/dotfiles (the Stow directory in this case) then ~/Desktop is the target directory. Some GNU Stow users prefer to clone their dotfiles repo at ~ so that they don't have to mention the target directory using the -t option!

So, lets move our dotfiles repo from ~/Desktop/dotfiles to ~.

mv ~/Desktop/dotfiles ~
cd ~
stow bash
# No need to mention the target directory using the `-t` option
# because the parent directory is the default target directory (`~` in this case).
  1. The command to unstow packages (to remove the symlinks) is,

    stow -D <package-name> -t <target-directory>
    

    To unstow kitty & bash,

    stow -D kitty bash -t ~
    

    To unstow vscode,

    stow -D vscode -t "~/Library/Application Support/Code/User" # In macOS.
    stow -D vscode -t "~/.config/Code/User" # In Linux.
    
  2. Putting it all together.

    We have 2 problems,

    1. We can't remember all the stow package names & their target directory.

    2. The target directory of some Stow packages depend on the operating system.

    To solve these 2 problems let's write a shell script that determines the computer's operating system & stows/unstows all of the stow packages to their target location.

    Create a shell script,

    touch stow.zsh
    chmod +x stow.zsh
    

    Paste the code given below in stow.zsh,

    #!/bin/zsh
    unstow=false
    for arg in "$@"; do
      if [[ "$arg" == "-D" ]]; then
        unstow=true
      fi
    done
    
    stow_unstow_package() {
      local package_name=$1
      local target=$2
    
      if [[ "$unstow" == true ]]; then
        echo "Unstowing package: $package_name"
        stow -t "$target" -D "$package_name"
      else
        echo "Stowing package: $package_name"
        stow -t "$target" "$package_name"
      fi
    }
    
    
    packages=()
    
    if [[ "$(uname)" == "Darwin" ]]; then
      # For macOS
      packages=("bash" "kitty")
      vscode_settings_dir="$HOME/Library/Application Support/Code/User"
    
    else
      # For Linux
      packages=("bash" "kitty")
      vscode_settings_dir="$HOME/.config/Code/User"
    fi
    
    
    if [ -d "$vscode_settings_dir" ]; then
        stow_unstow_package "vscode" "$vscode_settings_dir"
    fi
    
    for package in "${packages[@]}"; do
      stow_unstow_package "$package" "$HOME"
    done
    

    To stow your dotfiles,

    ./stow.zsh
    

    To unstow your dotfiles,

    ./stow.zsh -D
    

You can use a bare git repository to manage your dotfiles if you don't prefer creating symlinks.