10 minute read

If I could go back in time and tell my younger self a few things that would help me be a better software developer, it would probably be:

  • read the source, luke.
  • read papers.
  • create and maintain a dotfiles repo.

The first two now seem obvious. Reading the code of the projects you use gives you insight that you can’t get from reading the documentation. Reading papers like Amazon Dynamo fits the same bill and in that case, allows you to more fearlessly operate datastores. I often avoided doing both because it seemed hard, and like anything new it is hard until you’ve done it a bit and it becomes more natural.

The third entry, a dotfiles repo, is probably a little less obvious. But I really love my dotfiles repo. Dotfiles are a force multiplier for me. If you don’t have or use one, maybe I can convince you why you should.

what is a dotfiles repo?

A dotfiles repo is one that defines your terminal shell experience. This includes scripts to install and configure utilities you use frequently (e.g. ripgrep, homebrew, neovim, tmux, git, etc). It also is a nice home to your shell configuration (e.g. configuring ohmyzsh). Dotfiles are also where the random bash scripts you write live. Do you install systemd or launchd agents? They can live here too.

While these things are nice, the real power comes from the fact that all of this configuration is version controlled. I very frequently find that engineers will simply start fresh when they get a new job or a new laptop, and add the programs they need as the need arises. While this is wildly inefficient, it’s insidious in another way:

because machines are treated as ephemeral, the investment to customize them is not made.

This means that you are always using a particular tool as it comes out of the box. Which… is certainly fine. However, if you’re looking to increase your own power as a developer, you will want to configure these to bend to your will. And here’s the great thing:

a dotfiles lets you make tiny changes easily.

You don’t really need to configure everything about a particular tool. But if you set up your dotfiles to allow you to easily change the configuration, then you’ve just removed a huge mental roadblock. You know exactly where to make the change and you commit it. It’s there forever and you will never be without that adjustment unless you will it to be so. Over time, these adjustments will grow and eventually someone will call you a wizard because it looks like magic watching you control your machine.

Hopefully I’ve convinced you. Let’s chat about how to create a dotfiles repo and get in the flow of using it. After that I’ll show you some of the things I like best about mine.

create your own dotfiles

Creating a dotfiles is easy. There are many dotfiles frameworks like chezmoi that have an opinionated structure and rythym. These frameworks are great for what they do, but frameworks in general tend to make easy things easier, and more nuanced things much more difficult as you try to bend the framework to your will for use cases that were not originally envisioned.

Since many of them are simply bash scripts under the hood, here’s what I suggest:

create your own dotfiles repo using shell scripts

I know… 😱 but trust me. The level of scripting that we are going to do is very basic. And having some base level of proficiency with the lingua franca of computing has a huge return on investment. Maintaining your dotfiles will improve your shell scripting proficiency, and you’ll find that you’ll start writing one-off helpers in bash/zsh more often that will… live in your dotfiles!

So let’s just do it then.

cd ~
mkdir .dotfiles
cd .dotfiles
git init
echo 'echo "Starting .dotfiles install"' > install.sh
chmod 755 install.sh
git add -A
git commit -am "Initial commit."

Congrats, you now have a version controlled dotfiles 🎉.

You’ll want to be able to push changes to it so that on a new machine you can clone the repo and start using it. I’d really recommend creating a public repo. You’ll likely end up sharing your dotfiles with others or asking questions about a particular script. Having a private repo closes a lot of doors. You don’t want to put secrets in your dotfiles, and having an open source repo removes that possibility altogether.

Push your changes. We’ll now start adding to your dotfiles.

bootstrap your dotfiles

The advice I’ll give is based on how I use my own dotfiles repo: github.com/collinvandyck/dotfiles. Feel free to dig around. It may seem like a lot, but it’s not – it’s a lot of little things that just grew over time, like yours will.

When you created your dotfiles, you added an install.sh script in the root that just echos to stdout. Aside from editing the files in your dotfiles, this will be the main way you interact with your dotfiles. It will bring your machine to the desired end state – your custom development environment. It takes no arguments. You just run it.

./install.sh
Starting .dotfiles install

The first thing we’re going to do is to edit the install script to follow some bash norms:

#!/usr/bin/env bash

set -e # fail on error
set -u # error on undefined variables

echo "Starting .dotfiles install"

Without -e bash will happily keep going whenever an error happens. We want to fail fast so that we don’t continue when a command that subsequent ones depend on has failed. As you hit failures you will need to program around them, and handle the errors, depending on the context. This will make your dotfiles more relisient and will make you a better programmer.

The -u option has a similar effect, failing the script if you try to dereference variables (e.g. $MY_URL) when the variable MY_URL does not exist.

Commit your changes and push them. Years from now I’d suggest using something like lazygit to walk through your changes over time. It’s good to see your progression in this way. lazygit is a wonderful tool. Why don’t we add it to your dotfiles so you have it forever?

maintain your dotfiles

Let’s make our first change and add lazygit to your dotfiles. Starting out, keep it simple. Just append to install.sh. You’ll know when it’s time to break things up, but doing so right now is counterproductive.

I’m on a mac so I’ll be using the homebrew package manager. Add the following to your install.sh:

# If brew does not exist on this system, install it.
if ! command -v brew >/dev/null; then
    echo "Installing homebrew"
    # from: https://brew.sh/
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

Instead of just running the command to install brew every time, we first check to see if it’s installed. If you’re not familiar with the if conditional, command -v brew outputs the path of the brew command, or errors out. But what about the set -e we added earlier? If you are testing for the success of the command, as we are, an error will not fail, but will result in a true for our if statement because of the ! negation.

Run ./install.sh. If you didn’t have homebrew, you will now. Run it again, and it should not install it a second time. Commit and push. This is the kind of granular micro commit strategy that really works well for dotfiles.

And now, it’s time to install lazygit. Add this to your install.sh:

# update homebrew
brew update --quiet
# a sorted list of packages to install
brew install --quiet --formula \
    lazygit \
    ;

We use the \ syntax to break up a command into multiple lines. The goal here is to have each package live on its own line. This list will grow over time (I have about 70) and having one package per line means it’s easy to sort, and having a sorted package list is much easier to maintain.

Go ahead and run ./install.sh again. It should install lazygit. If it didn’t, fix the error. Once you’re done, commit and push. Again, small, focused commits.

having small focused commits makes it easier to roll back changes.

If you have a huge commit that touches many files, it gets more difficult to back out changes. And a dotfiles is in the business of making things easier.

At this point, just maintaining a set of installed and up to date homebrew packages on your machine and any new machine or vm that you may have in the future is a lovely dotfiles. If that’s all you want, that’s totally fine, and you can be happy with something like this. But if you push a little bit more the rewards get bigger.

linking configs

lazygit, and many other utilities, have their own config which lives in a separate file. Unlike many other utilities, lazygit on MacOS stores its config in ~/Library/Application Support/lazygit instead of ~/.config/lazygit which makes it a bit harder to access on the command line. Let’s make it easy.

What we will do is maintain the config for lazygit within our repo and then symlink that config into the above path. lazygit does not care that it’s a symlink. Then we can just make config changes locally in our repo and it’s automatically reflected via the symlink. You can, and should, use this pattern when possible for all of the tools you install in your dotfiles.

First, create the config file in a lazygit folder in the root of your dotfiles:

cd ~/.dotfiles    # nice
mdkir -p lazygit  # use -p so that it doesn't error out if it already exists.
cd lazygit
touch config.yml

A nice minimal config you can put in config.yml is:

git:
  commit:
    autoWrapCommitMessages: true
    autoWrapWidth: 80

Save the config.yml and then append the following to install.sh:

# create config dir if it does not already exist
mkdir -p ~/Library/Application\ Support/lazygit

# link lazygit config
ln -s -f ~/.dotfiles/lazygit/config.yml ~/Library/Application\ Support/lazygit/config.yml

Run ./install.sh. Fix any bugs caught by using set -e. Commit and push.

The -f option to ln tells it to overwrite the link if it already exists. If you didn’t have this, ln would crash your script when running install.sh at some point in the future. Idempotency is king when it comes to dotfiles install scripts.

Congrats, you now have a version controlled lazygit config. You can make changes in your dotfiles lazygit config instead of trying to remember the actual config lives. You can make changes and later roll them back if they are not working. Later on you’ll probably end up creating branches to test out more comprehensive changes so that you can back them all out easily.

linking shell init

Many tools will update your shell init (.zshrc, .bashrc, etc) when they install. Other tools, like homebrew will ask you to do so, to allow brew to work correctly in a new shell. After installing homebrew it suggests to add the following to your shell init:

eval "$(/opt/homebrew/bin/brew shellenv)"

Let’s add (or modify, if you’ve already added it) this to our .zshrc (modify for the shell you use), but with a tweak that we used before:

if command -v /opt/homebrew/bin/brew >/dev/null 2>&1; then
    eval "$(/opt/homebrew/bin/brew shellenv)"
else
    echo "Homebrew not installed"
fi

We check that homebrew is installed first, before performing the eval. You’ll want to add your dotfiles onto new machines and this avoids a shell error in the case where homebrew has not yet been installed.

Now that you’ve made that change, let’s add your shell init into your dotfiles! This has the additional benefit of you being able to see what changes other tools make when you install them. Since we’ll link .zshrc in the same way we did before, if some other tool changes your .zshrc it will show up as a modification in your git repo. You then have the power to accept the change or not.

Like we did for lazygit, create a zsh folder in your dotfiles repo and copy your shell init into it:

cd ~/.dotfiles
mkdir -p zsh

# copy your current .zshrc into your dotfiles .zshrc
cp ~/.zshrc ~/.dotfiles/zsh/.zshrc

Modify your install.sh:

ln -sf ~/.dotfiles/zsh/.zshrc ~/.zshrc

Commit your changes and push. Like lazygit, you can now also fearlessly make changes to your zsh configuration.

next steps

We’ve only touched the very surface, but this is a legitimate dotfiles that is all yours and only needs bash to run.

As you add to this you’ll probably also want to consider:

  • adding your git config to your dotfiles.
  • creating a bin folder for your helper scripts, and adding it to the PATH in your .zshrc.
  • being able to run install.sh on different operating systems, gracefully.

Your dotfiles will evolve over time. And eventually, someone will ask you where you went to wizarding school.

Go forth and dotfiles!


Special thanks to Ray Jenkins for providing helpful feedback

Updated: