For almost 10 years I’ve been using a home-baked dotfiles manager i put together using bash and an ancient version of node (v0.x). It has stood me in good stead. I’ve used it on various workstations and servers. It gives me access to all my aliases and preferences just by installing it and sourcing it in .bashrc. I can then enable the particular dotfiles that make sense in that environment. Usually as a reflex I enable prompt and aliases, giving me a nice-looking bash prompt and all the shortcuts I’m used to, so that I can continue to rely on muscle memory for common commands.

This weekend I decided to strip it of cruft, bring it up to a modern version of javascript (ES2020), and publish it to npm.

If you spend a lot of time on the command-line and don’t yet have a dotfiles manager, do yourself the favour. This has allowed me to keep shell startup nice and fast because dotfiles are broken up into logical modules, making it easy to enable or disable different dotfiles, depending on my needs at the time.

Keep $PATH short

A good example of how modularizing dotfiles helps is in the case of $PATH. $PATH is an environment variable that can grow really, really big and contain illegal and duplicated paths. That slows you down. If I am not hacking on Go, I don’t need go in my path. If I am not hacking on Rust, I don’t need cargo in my path. This principle can be generalized. Keeping startup files slim is good.

To illustrate, my Go dotfile:

GOPATH="$(go env GOPATH)"
GOBIN="${GOPATH}/bin"

dotfiles_add2path $GOBIN

dotfiles_add2path is a convenience function you get with the dotfiles utility. It does the same as this in bash: PATH=$PATH:/my/new/path;export PATH, but it comes with the sanity-checking that bash sorely lacks. Namely:

  1. If the path has already been added, it is silently ignored. This allows you to avoid having your path set as something crazy like /usr/local/bin:/usr/bin:/usr/bin:/usr/bin:/usr/bin
  2. if the new path is not a legal path or doesn’t exist, it panics.

Here’s my Rust dotfile:

source $HOME/.cargo/env

Nice and short, right? In the early days, Rust and Go needed a lot of environment variables to be set to work correctly. They have both gotten much better. Still, all I have to do now to have a shell that’s aware both those languages is:

dotfiles enable go
dotfiles enable rust
bash

The last command re-loads your shell. This could also be accomplished with any of these commands:

. ~/.dotfiles/bootstrap
. ~/.bashrc
dotfiles_reload

The effect of enabling/disabling is persistent. All subsequent shells will have go and rust enabled, meaning ~/.cargo/bin and ~/go/bin will be in my path, and the environment variables GOBIN and GOPATH will be available.

Fast visual identification

I often have terminal sessions on 3 or more computers at once. For example, my workstation, a server on AWS, and a docker container. It’s helpful to be able to tell at a glance which one I’m on. For this reason the prompt dotfile checks for a file called ~/.prompt_colours, and creates it using random colours if it doesn’t exist. This ensures that every machine has a unique-looking prompt. It reduces cognitive load by reducing confusion at a subconcious level. I can distinguish one server from another before my concious mind even needs to get involved. Here’s the code:

if [ ! -f ~/.prompt_colors ]; then
	function get_rand() {
		RANGE=256
		number=$RANDOM
		let "number %= $RANGE"
		printf "'%s'" "\e[38;5;${number}m"
	}
	printf "%s\n\n" "#!/bin/bash" >> ~/.prompt_colors
	printf "PROMPT_COLOUR_USER=%s\n" "$(get_rand)" >> ~/.prompt_colors
	printf "PROMPT_COLOUR_CHAR=%s\n" "$(get_rand)" >> ~/.prompt_colors
	printf "PROMPT_COLOUR_HOST=%s\n" "$(get_rand)" >> ~/.prompt_colors
	printf "PROMPT_COLOUR_DIR=%s\n" "$(get_rand)" >> ~/.prompt_colors
	printf "PROMPT_COLOUR_BRANCH=%s\n" $(get_rand) >> ~/.prompt_colors
	printf "%s" "PROMPT_COLOUR_RESET='\e[0m'" >> ~/.prompt_colors
fi
source ~/.prompt_colors

And then when I want to construct the prompt, I use those variables:

PS1="\[$(p $PROMPT_COLOUR_USER)\]\u \[$(p $PROMPT_COLOUR_CHAR)\]@ \[$(p $PROMPT_COLOUR_HOST)\]${PRETTY_HOSTNAME} \[$(p $PROMPT_COLOUR_CHAR)\]∈ \[$(p $PROMPT_COLOUR_DIR)\]\w"

Those are by far the most complex parts of Dotfiles, which at it’s core just a simple place to put your dotfiles and some convient ways to pick and choose which ones you want to load. There isn’t much cost to giving it a go. Dotfiles stays out your way until you need it. You can install it and bring your dotfiles in one by one, at your own pace.