Make Your dotfiles Portable With Git and a Simple Bash Script
Tags: bash, git, github, dotfiles
Published: 2020-12-23 04:46:48

---
title: Make Your dotfiles Portable With Git and a Simple Bash Script
series: Automating Your Developer Environment
author: Freddie Carthy
excerpt: Make syncing your dotfiles across systems one of your superpowers.
description: Syncing your dotfiles between systems shouldn't be a hassle. In this walk-through I'll show you how you can use Git and a simple, yet power Bash script to easily sync your dotfiles between multiple systems.
canonical_url: https://freddiecarthy.com/blog/make-your-dotfiles-portable-with-git-and-a-simple-bash-script
---

Are you getting the most out of your dotfiles? Time and again I see engineers under-utilizing the power of their dotfiles. They either don't maintain a version-controlled set of dotfiles, or they do but they haven't been touched in years. This can lead to a fragmented developer environment between your personal and work computer. **Wouldn't it be cool if you could have a one-line install for your dotfiles and a single command to keep them up-to-date between systems?**

My dotfiles are like my digital identity. They serve two primary purposes; they hold all of my settings and I use them to install all of my tooling. This keeps my work and personal computer 100% synced.

At minimum there's a handful of files that I like to share across systems. These include files like `.gitignore`, `.gitconfig`, bash aliases, `.bash_profile`, `.vimrc`, and a number of others.

There's also several tools that I always use regardless of the system I'm working on; Git, Vim, yarn, npm, and Homebrew to name a few.

In this series **I'll show you how to supercharge your dotfiles to keep them synced across your machines effortlessly.**

## 1. Setting things up.

**This script assumes you're using a Mac**. First things first you'll need to setup a repo on GitHub (or preferred location) for your dotfiles.

> I've set up a [sample repo here](https://github.com/gjunkie/dotfiles-starter-kit) if you just want to get going.

Clone the repo and `cd` into it. Create a folder called `/opt`, and in there a file called `files` (in the future this folder will also hold a list of tools you use). In that file paste the following (add any dotfiles relevant to you):

```
# /opt/files
configs/.gitconfig    .gitconfig
configs/.gitignore    .gitignore
configs/.bash_profile .bash_profile
configs/.vimrc        .vimrc
```

This list will tell our future `install` script to look for the files listed on the left and create symlinks in our home directory with the names on the right. We haven't setup the `/configs` folder to hold our dotfiles quite yet, so let's do that next!

Create a `/configs` folder at the root of the project. This folder is what will hold all of our dotfiles (as listed above). Think of these files as the "source of truth" for each of your dotfiles. For each of the files you can paste in the contents of your current dotfiles to get started. Make a commit and push this baby up! This is how things should look like now:

```
/dotfiles
  /opt
    files
  /configs
    .gitconfig
    .gitignore
    .bash_profile
    .vimrc
```

OK, moving on...

## 2. Creating a simple install script

Now we've got our dotfiles in our `/configs` folder and pushed to GitHub. Pretty great! But what we _really_ want to do is have an easy way to download these dotfiles onto a new system _without even having Git installed_.

Create a file called `install` at the root of the project. We'll need this script to do a couple of things for us:

1. Download the repo and set itself up in the `/usr/local/opt` directory. This is because we'll keep our local copy of the cloned repo where we make changes separate from the set of files your machine will use as the "source of truth".
2. Symlink our dotfiles in that directory from our home directory.

At the top of the file let's set up a few variables for easy use in our functions:

```
# /install
#!/usr/bin/env bash
LOG="${HOME}/Library/Logs/dotfiles.log"
GITHUB_USER=your github user name here
GITHUB_REPO=<your github repo name
DIR="/usr/local/opt/${GITHUB_REPO}"
```

Setting the `LOG` location to this will ensure that all logs are visible in the macOS Console app. Handy if something goes wrong and you need to check the logs. Make sure to set `GITHUB_USER` to your username and `GITHUB_REPO` to whatever you're calling your dotfiles repo. Let's also set up a couple of helper functions that will print out some fancy output as our script runs.

```
# /install
_process() {
  echo "$(date) PROCESSING:  $@" >> $LOG
  printf "$(tput setaf 6) %s...$(tput sgr0)\n" "$@"
}

_success() {
  local message=$1
  printf "%s✓ Success:%s\n" "$(tput setaf 2)" "$(tput sgr0) $message"
}
```

OK, so now we get to the fun part. Let's write a function that downloads our `dotfiles` repo so we can use it locally.

```
# /install
download_dotfiles() {
  _process "→ Creating directory at ${DIR} and setting permissions"
  mkdir -p "${DIR}"

  _process "→ Downloading repository to /tmp directory"
  curl -#fLo /tmp/${GITHUB_REPO}.tar.gz "https://github.com/${GITHUB_USER}/${GITHUB_REPO}/tarball/main"

  _process "→ Extracting files to ${DIR}"
  tar -zxf /tmp/${GITHUB_REPO}.tar.gz --strip-components 1 -C "${DIR}"

  _process "→ Removing tarball from /tmp directory"
  rm -rf /tmp/${GITHUB_REPO}.tar.gz

  [[ $? ]] && _success "${DIR} created, repository downloaded and extracted"

  # Change to the dotfiles directory
  cd "${DIR}"
}
```

What this is doing is creating a `/dotfiles` directory at `DIR`, downloading a tarball of your latest `main` branch, extracting the file, cleaning up, and finally changing your working directory to `/usr/local/opt/dotfiles`.

Now that we've downloaded your dotfiles we need a basic function to grab your dotfiles from the `/configs` folder and create symlinks to them in your home directory. Here it is:

```
# /install
link_dotfiles() {
  # symlink files to the HOME directory.
  if [[ -f "${DIR}/opt/files" ]]; then
    _process "→ Symlinking dotfiles in /configs"

    # Set variable for list of files
    files="${DIR}/opt/files"

    # Store IFS separator within a temp variable
    OIFS=$IFS
    # Set the separator to a carriage return & a new line break
    # read in passed-in file and store as an array
    IFS=$'\r\n'
    links=($(cat "${files}"))

    # Loop through array of files
    for index in ${!links[*]}
    do
      for link in ${links[$index]}
      do
        _process "→ Linking ${links[$index]}"
        # set IFS back to space to split string on
        IFS=$' '
        # create an array of line items
        file=(${links[$index]})
        # Create symbolic link
        ln -fs "${DIR}/${file[0]}" "${HOME}/${file[1]}"
      done
      # set separater back to carriage return & new line break
      IFS=$'\r\n'
    done

    # Reset IFS back
    IFS=$OIFS

    source "${HOME}/.bash_profile"

    [[ $? ]] && _success "All files have been copied"
  fi
}
```

Looks a bit complex, but what's happening here is actually quite simple. It looks at the `/opt/files` file we created, which lists each of your dotfiles, and loops through each line. It then creates a symlink in your home directory for each of those pointing to the files in your `/usr/local/opt/<repo name>/configs` folder.

## 3. Putting it all together

Finally, we need to actually run these functions for things to actually happen. Here's how I like to do that:

```
# /install
install() {
  download_dotfiles
  link_dotfiles
}

install
```

Place that snippet at the bottom of your `install` file and you're ready to go. **Keep in mind that running this script will replace any dotfiles you've listed in your `files` file!**

Now, running the following command in your terminal will execute our script:

```
$ bash -c "$(curl -#fL raw.githubusercontent.com/<your github username>/<your dotfiles repo name>/main/install)"
```

In the next _installments_ 😂 we'll expand on this by having it install our tooling as well as setting up an update script.

If you enjoyed this post, [I'd love to hear about it on Twitter](https://twitter.com/freddiecarthy)!