How to manage your dotfiles like a pro with stow?

Managing your dotfiles is a fairly common thing for developers. But more often than not for many, things “just work”. If duct tape really was a developer tool, it would surely be found here. But it should not be hard to handle your dotfiles like a pro. In this article I will show you how. how to manage your dotfiles like a pro.

Developers don’t often have to deal with managing dotfiles on windows. So it might be only items such as ssh, orchestration tools like docker, and language specific things like environments in python, build targets for Rust, or setting up Go. However these tools can still be automated. Stow has other advanced usages and way to run it. On windows, you can use a tool called dploy mentioned later.

GNU Stow

First thing is first. Install the stow binary on your platform of choice.

OSX

with brew installed, you can simply run:

brew install stow

Boom you’re done! pretty simple.

Debian/Ubuntu (and derivatives)

Easy as well.

sudo apt install stow

Arch

Just as simple

sudo pacman -S stow

Creating your Dotfiles

Now let’s create a folder to get started. You can manage your dotfiles repo anywhere. I keep it along side my other code in ~/code/dotfiles. So from here on, I will just refer to it as $DOT. If you want to follow along, just run this in your terminal:

export DOT=$HOME/code/dotfiles

Just replace the path to where your dotfiles are located. A common location I see others use is $HOME/.dotfiles. Now when you paste code in a bash compliant shell it will still work. While in this directory, lets first create some directories and files

mkdir -p __setup git termite/.confg bin/bin bash
touch setup.sh

this will create a few directories in your dotfiles.

  • __setup: used for one time setups, such as package installation on different platforms.
  • git: this will be a place we can save out global git configurations like .gitconfig
  • termite/.config: this will be where we store a configuration for a terminal named termite. I am using termite here to help show how the folders are structured and why.
  • bin/bin: is where you will save command line binaries made in bash so they are available system wide. this directory will be added to your $PATH
  • bash: where we will save our bash configuration.

Git

This is simple, we want to manage and keep track of our Git configuration in our dotfiles. So we will move our global .gitconfig file into this directory

mv ~/.gitconfig $DOT/git

Now we have $HOME/code/dotfiles/git/.gitconfig. We need to use stow to install it. Lets do that. From inside the $DOT directory, run this:

 ➜ stow -v -R -t ~ git
LINK: .gitattributes => code/dotfiles/git/.gitattributes
LINK: .gitconfig => code/dotfiles/git/.gitconfig

-v just means verbose so we can see what it is doing. -R means recursive, which we don’t really need here, but habits, and -t ~ is the target, or where this stow should be installed to. finally, we specify the directory git for what directory we are installing.

 ➜ ls -la | grep .git
lrwxrwxrwx    32 shawn 17 Jun 23:05  .gitattributes -> code/dotfiles/git/.gitattributes
lrwxrwxrwx    28 shawn 17 Jun 23:05  .gitconfig -> code/dotfiles/git/.gitconfig

As we can see above, it placed the files back into the home directory and symlinked them. Note that this does not work on windows but will work inside the WSL environment and does quite well.

Automate the Dotfiles

So, we know we are going to need to do this more. And if you have to do it more than once you should automate it right? Of course. Automation is the key to managing repetitive tasks so we will use a bash script to install our dotfiles folders.

https://imgs.xkcd.com/comics/automation.png
courtesy xkcd https://xkcd.com/1319/

Open up setup.sh and place in it the following contents:

#!/usr/bin/env bash

# make sure we have pulled in and updated any submodules
git submodule init
git submodule update

# what directories should be installable by all users including the root user
base=(
    bash
)

# folders that should, or only need to be installed for a local user
useronly=(
    git
)

# run the stow command for the passed in directory ($2) in location $1
stowit() {
    usr=$1
    app=$2
    # -v verbose
    # -R recursive
    # -t target
    stow -v -R -t ${usr} ${app}
}

echo ""
echo "Stowing apps for user: ${whoami}"

# install apps available to local users and root
for app in ${base[@]}; do
    stowit "${HOME}" $app 
done

# install only user space folders
for app in ${useronly[@]}; do
    if [[ ! "$(whoami)" = *"root"* ]]; then
        stowit "${HOME}" $app 
    fi
done

echo ""
echo "##### ALL DONE"

In the code above, we will install the git directory for only the local user as root doesn’t need that. However bash which we will do next, can be used for both local users and root. We then create a bash function named stowit to run the actual stow command with our required arguments. If we were to call this when we installed git, it would be called as

stowit ~ git

which you can see in the loops just after that when I call stowit. It’s rather simple really. The first loop is to install folders for any user, and the second has a check to install for any user unless it is the root user. So lets setup the bash directory.

mv ~/.bashrc $DOT/bash
mv ~/.bash_profile $DOT/bash
mv ~/.profile $DOT/bash

OSX may not have all of these, Windows for sure does not. If you don’t have them, then you can ignore them.

Run the setup

The bash files are now being managed in your dotfiles repo. So now lets try running our setup file.

 ➜ chmod +x setup.sh
 ➜ ./setup.sh
Stowing apps for user:
LINK: .profile => code/dotfiles/bash/.profile
LINK: .bashrc => code/dotfiles/bash/.bashrc
LINK: .bash_profile => code/dotfiles/bash/.bash_profile
UNLINK: .gitattributes
UNLINK: .gitconfig
LINK: .gitattributes => code/dotfiles/git/.gitattributes (reverts previous action)
LINK: .gitconfig => code/dotfiles/git/.gitconfig (reverts previous action)


##### ALL DONE

You can see that stow is pretty smart about linking our files and folders. It linked our new bash files. But when we ran stow again it went through our previously linked git files, re re-linked them. You can actually configure how that handles those situations with different flags. stow will also abort stowing folders when it finds new files that have not been stowed before and will tell you what files so you can fix them. If we were to run

sudo ./setup.sh

then only the bash files would be setup in the roots home directory. This is nice because often times when we have to change to the root user we lose all the cool setups we have done for our user.

The bin directory

We have two last files in our setup. bin will be easier so lets do that. inside the $DOT/bin/bin folder we can place any binary files we want to keep around across systems. Just make sure they are cross platform. This is why I like using bash, and Go binaries since you don’t need to worry about dependencies. Python is the same but it can depend on what version of python is available. As much as I love python for things, bash is best here unless there’s some magic tools in python to do what we need. Like processing CSV files or manipulating data.

In the $DOT/bin/bin folder, lets create a file named $ … YES! Just a dollar sign. You may be asking why, but you will see. Update it’s contents to be:

#!/bin/zsh

# Ever pasted "$ somecommand" into the terminal and gotten this error?
# -bash: $: command not found

# Begone, silly errors! Lazy copy + paste forever!! ETCETERA!!!!

echo 'Quit pasting in commands from the internet, you lazy bum.'
"[email protected]"

Then, in $HOME/.bashrc simply add this line:

export PATH="$HOME/bin/$:$PATH"

Run setup.sh again to link the files. Once linked we need to update our bash terminal environment. You can either restart your terminal, or run

source ~/.bashrc

to do the trick. You should be able to type:

 ➜ which $
/home/shawn/bin/$

If so, then it works. Now say you are copying and pasting a command from stack overflow. And you accidentally paste in the $ from their terminal. Which usually denotes where their prompt starts. Normally, you would get some kind of command not found error for the $ command. But now, if you take the command $ echo "I work now" and paste it into your terminal, you get:

 $ echo "I work now"
Quit pasting in commands from the internet, you lazy bum.
I work now

Now when you copy a command from a code sample and also copy the $ character, it will continue to run the command but will also give a message (if you wish) about making sure not to copy the $. Making it not break in many cases. Now, in the $HOME/bin directory, you can place any commands you want available system wide.

Termite

Now lets do termite. I mostly have this here to help you understand how it copies directories over. So if you don’t use termite, just read along.

The program termite keeps it’s configuration file not in $HOME, but in $HOME/.config/termite. Or, more specifically, $XDG_CONFIG_HOME/termite as $XDG_CONFIG_HOME usually defaults to $HOME/.config anyways.

When we run stow -R -t ~ termite it takes the source directory, in this case $DOT/termite and maps it’s contents to the target directory, which is ~ aka $HOME. For me that is /home/shawn. On OSX it would be /Users/shawn. Inside the termite directory is a .config folder. Since $HOME/.config is already there, we must go one level deeper.

https://cdn-images-1.medium.com/max/1600/1*cwR_ezx0jliDvVUV6yno5g.jpeg

Now it will compare $DOT/termite/.config/termite with $HOME/.config/termite and see that that it doesn’t exist yet in $HOME/.config/termite and will essentially run

ln -s $HOME/code/dotfiles/termite/.config/termite $HOME/config/termite

And when we run setup.sh again, we can see this is true

 ➜ pwd
/home/shawn/.config

 ➜ ls -la | grep termite
lrwxrwxrwx    40 shawn 14 Jun  9:37  termite -> ../code/dotfiles/termite/.config/termite

Conclusion

I have showed you how to manage your dotfiles like a pro. It is pretty simple. I haven’t yet talked much about the _setup directory we made. You can keep other setup files for your system that should only be run once. I use _setup/osx.sh for example to install homebrew and homebrew packages, and setup other system settings. I also have _setup/arch.sh for installing my packages using pacman or trizen.

From here, you can see how this simple setup can make it much easier to manage your dotfiles for linux and osx. A few extra steps needed to get them to work in windows. You could use something like https://github.com/arecarn/dploy which is a python port of stow but needs to have python installed on your system (please use python 3.6+). It knows how to handle things like environment variable between bash ($VAR) vs windows (%VAR%) as well as using a compatible system link. You may not use termite on windows, but some are using tools like Hyper Term which despite being built on electron, is a pretty good cross platform terminal.

You should easily now be able to take what you have for your configs like vim/neovim, tmux, or anything else and just place them into your dotfiles. If you want to use a submodule for something like tmux, just do

git submodule add [tmux-repo] tmux/

Then when running the setup.sh script it will make sure the code is local and link everything accordingly.

Looking for an Expert Developer?

Just send me a message!