Dotfiles for Greenhorns
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 thePATH
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