For the past 5 years or so, I’ve been writing a doctor script for every project I work on. I use it to make sure that the development environment is set up correctly, both when someone checks the project out on their computer for the first time, and any time the envrionment needs to change due to a change in the code.

For me, the doctor script is an alternative to readme files (which get out of date quickly because they are only used when a new person joins the project), tools like Ansible, Docker, and Chef (which are complicated, rigid, and add an extra layer of complexity), and emails and slack messages (which people forget to check especially when they just joined the project). But most importantly, it is an alternative to people hesitating to make changes to code because they are worried about breaking other people’s development environments.

(I created my first doctor script with my former colleague Jacob Maine. I don’t remember if I came up with the idea, if he came up with the idea, or if we heard about it from elsewhere. I’m guessing that brew doctor was a thing back then so that might have been at least part of the inspiration.)

The doctor script performs checks and then suggests remedies. The key is that the remedies are just suggestions, leaving the actual decision to the human. This avoids the complexity of having to consider all posibilities, and the rigidity of forcing a solution to a problem. When provisioning production servers, you don’t want a human to have to make decisions, but when setting up a development computer, you do. For example, doctor might suggest installing a new version of Postgres with Homebrew, but you might prefer to have Postgres installed with Asdf so that you can have multiple versions.

Here’s a typical doctor session:

% bin/dev/doctor 

[checking] homebrew: bundled?... OK
[checking] direnv: installed... OK
[checking] direnv: .envrc file exists... OK
[checking] asdf: installed... OK
[checking] asdf: erlang plugin installed... OK
[checking] asdf: elixir plugin installed... OK
[checking] asdf: nodejs plugin exists?... OK
[checking] asdf: tools are installed... OK
[checking] deps: elixir deps installed? (needed for yarn to compile)... OK
[checking] yarn: up to date... OK
[checking] phantomjs: installed... OK
[checking] postgres: launchctl script is linked... OK
[checking] postgres: running... OK
[checking] postgres: role exists... FAILED

Possible remedy: createuser -s postgres -U $USER
(it's in the clipboard)

I will usually run doctor in my update script to make sure my computer is still correctly configured to run the project, and sometimes in my start script to make sure the project will actually start. (See my article about development scripts for more info on update, start, and other scripts I add to every project.) I’ll also run it when things don’t seem to be working.

I typically write doctor scripts in Bash since it’s ubiquitous and typically doesn’t require any configuration or installation.

My doctor scripts are a series of calls to a check function, which look like this:

check "postgres: role exists" \
  "psql -A -c '\du' postgres | grep 'postgres|'" \
  "createuser -s postgres -U \$USER"

The first argument is the description, which gets printed out before the command gets executed. The second argument is the command. The third is a suggested remedy that will be printed out (and copied to the clipboard) in case the command fails.

The full script, with the implementation of the check function, looks like this:

#!/usr/bin/env bash

NC='\033[0m' # No Color

check() {

  echo -n -e "${CYAN}[checking] ${description}${WHITE}... ${NC}"

  eval "${command} > .doctor.out 2>&1"

  if [ $? -eq 0 ]
    echo -e "${GREEN}OK${NC}"
    return 0
    echo -e "${RED}FAILED${NC}"
    cat .doctor.out
    echo -e "${CYAN}Possible remedy: ${YELLOW}${remedy}${NC}"
    echo -e "${CYAN}(it's in the clipboard)${NC}"
    echo ${remedy} | pbcopy
    exit 1

check "postgres: running" \
  "psql -l" \
  "brew services start postgresql"

check "postgres: role exists" \
  "psql -A -c '\du' postgres | grep 'postgres|'" \
  "createuser -s postgres -U \$USER"

# ... more checks here ...

In my projects, I typically check:

Releated Reading