Tidbits on software development, technology, and other geeky stuff

dotfiles

In the last couple of days I have had this sinking feeling that something is going to happen to my MacBook. Like, it’s going to get stolen or it’s going to crash. It’s completely irrational but I’ve taken advantage of the opportunity to do a hypothetical postmortum to determine what I would have done differently.

I do a fairly good job of keeping important things in a safe, backed up location. All of my code is under soure-control, I use LastPass for passwords and software license keys, and I use Google for most everything (Email, Docs, Sheets, Music, Photos) so I don’t have to worry about backing up much.

However, I did realize I would be in a bit of a mess if I lost my “dotfiles”. You know, all the .filename files in your $HOME folder that many apps use to store your configuration. I have haphazardly backed up some of the more important ones like .psqlrc (PostgresSQL) but many aren’t backed up. I also have a handful of shell scripts I’ve accumulated that do various things like my beloved ws script which starts a web server from the local directory and fires up Chrome, pointing to the URL. If I lost this stuff, I would be sad; super sad.

Creating a dotfiles repo on GitHub is something I think is a pretty great idea. I’ve been meaning to do this but just kept putting it off. The reason I like using a GitHub repo for these is because they are versioned, easily sharable, and restorable. There are lots of example repos to go off of and you can learn a lot but just looking through them. For my purposes, I wanted something pretty simple and able to gracefully handle secrets.

The final result is here on GitHub: https://github.com/bradymholt/dotfiles

The repo structure is fairly flat and simple:

bin                 # directory containing shell scripts and other misc
                    #   executable utilities; will be symlinked from $HOME/

secrets             # secrets are stored here - this folder is not in the repo
                    #   but is cloned from a private Git repo during setup

.zshrc              # dotfile will be symlinked from $HOME/
.gitconfig          # dotfile will be symlinked from $HOME/
.crontab            # crontab which will be installed during setup
[...]
dotfiles-setup.sh   # the idempotent setup script

The dotfiles-setup.sh is where all the magic happens:

  1. Initialize secrets repo - A prompt will be given to enter the secrets repo URL. This git repo will be cloned to $DOTFILES_PATH/secrets and symlinked from $HOME/secrets. Obviously, you’ll want to use a git repo that is not publically available. As an example for how this is used, I have a var file in my secrets/ repo containing secret environmental variables. This secret file is loaded in my .zshrc.
  2. symlink files from ${HOME} to $DOTFILES_PATH/ - All top level objects in $DOTFILES_PATH will be symlinked from $HOME (with exception of .git/, and dotfiles*).
  3. symlink external config - This is where other symlinks are created for application specific needs. For example, BetterSnapTool stores its config in ${HOME}/Library/Preferences/com.hegenberg.BetterSnapTool.plist and I have a dotfiles config located at $HOME/.BetterSnapTool (symlinked to $DOTFILES_PATH/.BetterSnapTool in step #2 above!). During this step, a symlink is created: ${HOME}. /Library/Preferences/com.hegenberg.BetterSnapTool.plist > $HOME/.BetterSnapTool. This step allows me to keep my dotfiles symlinked under $HOME and then do additional, one-off linking when necessary.
  4. symlink ssh key to $HOME/secrets/id_rsa[.pub] - Symlink my keypair to the secrets folder. I know, Edward Snowden would be horrified.
  5. Setup crontab - The .crontab file is installed. Unfortunately OS X 10.11 (at least) seems to have a security mechanism that prevents automatic installation of crontabs. So, the setup script will install the crontab but then run crontab -e and instruct the user that a material change must be made to the crontab for it to be full installed.

dotfiles-setup.sh

#!/bin/bash

echo "#######################"
echo "BRADY'S DOTFILES SETUP"
echo "#######################"

DOTFILES_PATH=$(cd `dirname $0` && pwd)
CURRENT_SCRIPT_NAME=${0##*/}

LINK_TARGET_EXISTS_HANDLING=""
while true; do
    read -p "$(echo -e 'If files exist or are already symlinked, do you want to replace?\nAnswer [y]es, [n]o, or [p]rompt: ')" yn
    case $yn in
        [Yy]* ) LINK_TARGET_EXISTS_HANDLING="f"; break;;
        [Nn]* ) LINK_TARGET_EXISTS_HANDLING=""; break;;
        [Pp]* ) LINK_TARGET_EXISTS_HANDLING="i"; break;;
        * ) echo "Please answer: ";;
    esac
done

echo ""
echo "STEP 1: Initialize secrets repo"
SECRETS_FOLDER="${DOTFILES_PATH}/secrets"
if [ -d "${SECRETS_FOLDER}" ]; then
  echo "The secrets repo has already initialized!"
else
  read -p "$(echo -e 'The secrets repo needs to be initialized.\nEnter the secrets repo URL (i.e. https://gist.github.com/bradyholt/123456): ')" SECRETS_REPO_URL
  git clone $SECRETS_REPO_URL $SECRETS_FOLDER
fi

# [Re]create symbolic links from $HOME to ./*
# Only top level files/directories will be symlinked
echo ""
echo "STEP 2: symlink files from ${HOME} to ${DOTFILES_PATH}/"
find $DOTFILES_PATH -maxdepth 1 -mindepth 1 \( ! -iname "dotfiles*" ! -iname ".git" ! -iname ".gitignore" ! -iname "README.md" \) -exec ln -sv${LINK_TARGET_EXISTS_HANDLING} {} $HOME ';'

echo ""
echo "STEP 3: symlink external config"
# VS Code
ln -sv${LINK_TARGET_EXISTS_HANDLING} "${HOME}/.vscode.settings.json" "${HOME}/Library/Application Support/Code/User/settings.json"
ln -sv${LINK_TARGET_EXISTS_HANDLING} "${HOME}/.vscode.keybindings.json" "${HOME}/Library/Application Support/Code/User/keybindings.json"

# BetterSnapTool config - The format of the config is complicated so if something ever goes wrong and manual config is needed, here is a screenshot of the important settings: https://cloud.githubusercontent.com/assets/759811/15751225/f76b950c-28ae-11e6-9fd9-7c83aad698b2.png
ln -sv${LINK_TARGET_EXISTS_HANDLING} "${HOME}/.BetterSnapTool" "${HOME}/Library/Preferences/com.hegenberg.BetterSnapTool.plist"

echo ""
echo "STEP 4: symlink ssh key to ${HOME}/secrets/id_rsa[.pub]"
#Key pairs
ln -sv${LINK_TARGET_EXISTS_HANDLING} "${HOME}/secrets/id_rsa" "${HOME}/.ssh/id_rsa"
ln -sv${LINK_TARGET_EXISTS_HANDLING} "${HOME}/secrets/id_rsa.pub" "${HOME}/.ssh/id_rsa.pub"

echo ""
echo "STEP 5: Setup crontab"
echo "backing up user crontab to /tmp/crontab.bk"
crontab -l > /tmp/crontab.bk
echo "Replacing user crontab with contents of ${HOME}/.crontab"
crontab ${HOME}/.crontab
echo "NOTE: Because of an OS X security block, you will need to manually make a material change to your crontab before it will be installed."
echo "Your crontab will be opened next; you should make a material change (i.e. add a comment on a new line) and save it"
read -p "Press any key to continue"
crontab -e

Discuss on Twitter