How to: dotfiles

March 27, 2019
Written by

7iwai15WuuZ-1YLS6UzvVdC4vtbpjLMIoG8kABoHmrNLMLRwOxNJIsM4IFe20iQmGgZ48Co2rb42XjRcxRYNS5XwR04wcyrHF_18_aRkw-9BovIx3biXx07qt45DUkzDvIAUM_NW

Want to learn how you can setup your workstation and start developing as quickly as possible? Want all of your configurations saved somewhere?

Kikstart cartridge illustrating how to bootstrap a new system with dotfiles

What are dotfiles?

Dotfiles are the colloquial name for a user's configuration files. Historically many of them started with a period, making them invisible in *nix environments.

These files preserve your favorite settings on different programs that you use on your system. If you get a brand new system (or your current system is wiped out), you can just pull your dotfiles from the cloud, a USB stick, or even a floppy disk and restore all of your applications, binaries, and configurations for important applications and tools. A perfect example for developers who use git is your ~/.gitconfig:

user]
  email = kkrauss@twilio.com
  name = Kevin Krauss
[alias]
  ll = pull origin
  sh = push origin
  pf = push --force-with-lease
  pretty = log --graph --pretty=format:'%Cred%h%Creset %an: %s - %Creset %C(yellow)%d%Creset %Cgreen(%cr)%Creset' --abbrev-commit --date=relative
[merge]
  tool = meld
[core]
  editor = subl -w -n
  excludesfile = /Users/kkrauss/.gitignore_global
  hooksPath = /Users/kkrauss/.git-hooks
[diff]
  tool = meld
[mergetool]
  keepBackup = false
[pull]
  rebase = true
[rebase]
  autosquash = true
  autostash = true

If you do use version control: dotfiles that don’t have any sensitive information are great to commit to your own repo. Git is great for tracking system changes, just like it’s great for tracking code. As you go about your work, you can update your dotfiles and be ready to reinstall your system at a moments notice.

Dotfiles are great for teams as well, who work on many of the same projects and require many of the same tools. A team dotfiles repository can be created with member forks that contain personal configurations.

With shared dotfiles, we can draw the owl once, and everyone can benefit.

Draw the Owl

 

How can you create useful dotfiles?

You can:

Once you have a place to start, look through the template that you have chosen and get familiar with it.

What does the install script do? The install script should be something like: source install.sh or ruby install.rb.

Let's break down the install script we use for the API team at Twilio (run using ruby install.rb) looks like this:

#!/usr/bin/env ruby
require 'readline'
require 'erb'

First we import some gems. The first is readline, which will help us get the username. With ERB we can have some templates for files that we will customize (now we can put their username in some files).

Here we are prompting the user for their username. We could ask if they want certain packages installed that are optional, or what their favorite color is to theme their terminal.

puts "Welcome to the API team! Please fill in the details as prompted"
@user_name = Readline.readline("Username: ")
puts "Answer [Yy]es or [Nn]o"
bash_it = Readline.readline("Install bash_it? (autocompletion, themes, aliases, custom functions, and more)")

Setup some constants for use later in the script:

CODE_PATH = "#{ENV['HOME']}/code"
GITHUB_DOMAIN = "github.com"
BASE_REPO = "git@#{GITHUB_DOMAIN}:twilio"

Here is the super fancy erb rendering, where we can put their username and email in their gitconfig and use the username for the HOME name for log rotate setup on Mac.

def render_erb_file(file)
  results = ERB.new(File.read("#{file}.erb")).result(binding)
  File.open(file, "w+") do |f|
    f.write results
  end
end

render_erb_file("newsyslog.d/development.conf")
render_erb_file("git/gitconfig")

Check if the user responded yes to questions that you asked about:

def install?(str)
  if str[/^y(es)?$/i]
    if block_given?
      yield
    end
  end
end

Setup a clone repo function to help clone many repos later:

def clone_repo(name)
  system("git clone #{name}")
end

It is good to check if things are installed, either by checking if the directory or file exists (like we do here) or by checking if a command exists.

if !Dir.exists?("#{ENV['HOME']}/.asdf")
  system("git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.6.3")
end

if !File.exists?("/usr/local/bin/brew")
  system("ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"")
  system("ln -s $HOME/.dotfiles/git/gitconfig.work $HOME/.gitconfig")
  system("ln -s $HOME/.dotfiles/git/gitignore_global $HOME/.gitignore_global")
  system("ln -s $HOME/.dotfiles/git/git-hooks $HOME/.git-hooks")
  system("ln -s $HOME/.dotfiles/system/bash_profile $HOME/.bash_profile")
  system("ln -s $HOME/.dotfiles/Sublime\ Text\ 3/Preferences.sublime-settings $HOME/Library/Application\ Support/Sublime\ Text\ 3/Packages/User/Preferences.sublime-settings")
  system("ln -s $HOME/.dotfiles/iterm/profile $HOME/Library/Preferences/com.googlecode.iterm2.plist")
end

install?(bash_it) do
  if !Dir.exists?("#{ENV['HOME']}/.bash_it" )
    system("git clone --depth=1 https://github.com/Bash-it/bash-it.git ~/.bash_it")
    system("ln -s $HOME/.dotfiles/bash_it/themes/powerline-plus $HOME/.bash_it/themes/powerline-plus")
  end
end

Here we update Homebrew and then call brew bundle which will use the Brewfile to install all the taps, casks, binaries and App Store applications we have specified:

# Update Homebrew recipes
puts "Updating brew"
system("brew update")
puts "Running brew bundle..."
system("brew bundle")

To start contributing for your team, you need to create an SSH key and then paste it up on github. This will create your key and put it in your clipboard, then open Github to the page where you can paste your key.

if !File.exists?("#{ENV['HOME']}/.ssh/id_rsa")
  puts "Creating ssh key..."
  system("ssh-keygen -t rsa -b 4096 -C \"#{@user_name}@twilio.com\" -f $HOME/.ssh/id_rsa)
  system("pbcopy < $HOME/.ssh/id_rsa.pub")
  puts "Key copied to your clipboard. Please press \"Add SSH key\" on GitHub and paste your SSH Key. Press any key to open GitHub."
  STDIN.getch
  system("open -a \"Google Chrome\" https://#{GITHUB_DOMAIN}/settings/keys")
end

Using asdf we will install python at a specific version and set it to the default.

system("asdf plugin-add python")
system("asdf install python 2.7.15")
system("asdf global python 2.7.15")

Now, we will clone repos and run setup tasks on certain repos:

puts "Cloning repos..."
system("mkdir -p #{CODE_PATH}")
Dir.chdir(CODE_PATH)
clone_repo("#{BASE_REPO}/foo.git")
Dir.chdir("foo")
system("make install")
Dir.chdir(CODE_PATH)
clone_repo("#{BASE_REPO}/bar.git")
clone_repo("#{BASE_REPO}/baz.git")

Finally, there is an optional step here where we take a file that is filled with different Mac defaults we would like to set.

For instance: defaults write com.apple.print.PrintingPrefs "Quit When Finished" -bool true will quit the printer app when printing completes.

https://www.defaults-write.com/ is a good place to find things that you can customize. This macos file could be a dotfile, but it is only run once and only by this install script so lives in your dotfiles repo.

# Set macOS preferences
puts "Setting mac preferences"
Dir.chdir(CODE_PATH)
system("source macos")

One thing that we do on the API team is make symbolic links to the files in the dotfiles directory to paths on our systems. This way you can edit the files where they live, and your changes will simultaneously be updated in your ~/.dotfiles as well. This makes it easy to keep track of what has changed as well as commit new changes.

Useful tools to compliment dotfiles

Another thing you’ll like is the Brewfile. (I learned about the Brewfile recently myself!) Before Brewfile I was using Ansible, which has a Homebrew task.

The thing I like about the Brewfile is how easy it is to update: brew bundle dump will create a new Brewfile based on the things that you have already brew installed!

Picture of a brew for Brewfiles

Next we have asdf, another thing that I recently discovered. Many times I’ve thought: “Why do I have rvm, gvm, nvm, pyenv... Why isn’t there just one version manager to rule them all?”

One version manager to rule them all

After a tireless journey through distant lands, I'm happy to report that such a thing exists! The one ring: asdf

With asdf,  I can install all the language and version combinations I want, in the same way. No longer will I lose my mind figuring out how to install X-language at version Y while managing my path.

Finally, we simply create an SSH key for the user, copy it to their clipboard, and open Chrome with the url where they can paste their key. After that we clone some repos (foo, bar, and baz in our example) and perform make install on those repos, which does the rest of the job of making sure each program has all its dependencies and is ready to run.

The cool thing is if you have complex setup tasks, replicating the same environment across multiple different workstations is straightforward.

Conclusion: start to use dotfiles

Dotfiles are awesome and everyone should use them. They help simplify your setup, make team collaboration easier, and recover from computer problems. For a similar rant (and to see where my dotfile journey began) visit https://driesvints.com/blog/getting-started-with-dotfiles/

Kevin Krauss is an engineer on Twilio's API team and is currently working on setting up edge services for websockets. He enjoys fishing and foraging. If you have any questions, feel free to contact him kkrauss@twilio.com.