Shell script that copies configuration and dotfiles to repository

A while ago I had the idea to store all the important configuration and dotfiles from my system in a Git repository. That saved me a few months ago: My harddrive died, but I could get back to work with Ubuntu and that repository quickly.

Only problem: I maintained that repo by manually copying the files from the various directories to the repo directory. That’s what I wrote the shell script for.

What does the script do?

1. Checks whether the command is executed from within the root of a git repository
2. Detects which OS it’s operating on (Windows, Linux or OS X) and saves that information in a variable OS
3. Assigns variables for certain tool groups (like Bash, Git, etc.) and creates the target directories in the repo if they don’t exist
4. Copies file by file to these destinations

Notes:

• Currently, I separate the files on a per-OS basis. This is probably unnecessary and even a hinderance. I’d like to here your opinion here
• The script currently does not use any Bash >4 features, because besides Ubuntu I mainly work on Windows. I use Git Bash there which only has version 3 bundled.
• I also wrote a short article about this script: Backup configuration and dotfiles with Git and a shell script
• If you’re interested in current/updated versions, check out the script in the repo: get-dotfiles.sh
#!/bin/bash

################################################################################
#
# Shell script to copy important configuration files from the current
# environment to this repository.
#
################################################################################

# Exit as soon as a command fails
set -e

# Accessing an empty variable will yield an error
set -u

# Assume the working directory is the path to the dotfiles repository
REPO_PATH="$PWD/" # Abort mission when we’re not in a git repository if [[ ! -d .git ]] || ! git rev-parse --git-dir > /dev/null 2>&1; then echo "Not a git repository." exit 1 fi # Detect OS (OS X, Linux or Windows) if [ "$(uname)" = "Darwin" ]; then
echo "Detected system: OS X"

OS="osx/"

ST_DIR="$HOME/Library/Application Support/Sublime Text 3/Packages/User/" elif [ "$(expr substr $(uname -s) 1 5)" = "Linux" ]; then echo "Detected system: Linux" OS="linux/" ST_DIR="$HOME/.config/sublime-text-3/Packages/User/"
elif [ "$(expr substr$(uname -s) 1 10)" = "MINGW32_NT" ]; then
echo "Detected system: Windows"

OS="win/"

ST_DIR="$HOME/AppData/Roaming/Sublime Text 3/Packages/User/" NPM_DIR="$HOME/AppData/Roaming/npm/node_modules/npm/"
fi

# Adjust destination paths so the files are separated by OS
BASH_DEST="${REPO_PATH}${OS}bash/"
GIT_DEST="${REPO_PATH}${OS}git/"
NPM_DEST="${REPO_PATH}${OS}npm/"
RUBY_DEST="${REPO_PATH}${OS}ruby/"
ST_DEST="${REPO_PATH}${OS}sublime-text/"

declare -a destinations=($BASH_DEST$GIT_DEST $NPM_DEST$RUBY_DEST $ST_DEST) for dest in "${destinations[@]}"; do
mkdir -p "$dest" done # Bash cp "$HOME/.bashrc" "$BASH_DEST" cp "$HOME/.bash_aliases" "$BASH_DEST" # Git cp "$HOME/.gitignore_global" "$GIT_DEST" cp "$HOME/.gitconfig" "$GIT_DEST" # Node.js/npm cp "${NPM_DIR}.npmrc" "$NPM_DEST" # Ruby/RubyGems cp "$HOME/.gemrc" "$RUBY_DEST" # Sublime Text cp "${ST_DIR}Preferences.sublime-settings" "$ST_DEST" cp "${ST_DIR}Markdown.sublime-settings" "$ST_DEST" cp "${ST_DIR}YAML.sublime-settings" "$ST_DEST" cp "${ST_DIR}Package Control.sublime-settings" "$ST_DEST" cp "${ST_DIR}For Loop (range).sublime-snippet" "$ST_DEST" cp "${ST_DIR}Fraction (TeX).sublime-snippet" "$ST_DEST" # OS specific copy operations if [ "$OS" = "osx/" ]; then
echo "Nothing here."
elif [ "$OS" = "linux/" ]; then cp "${ST_DIR}Default (Linux).sublime-keymap" "$ST_DEST" elif [ "$OS" = "windows/" ]; then
cp "${ST_DIR}Default (Windows).sublime-keymap" "$ST_DEST"
fi

echo "Completed."


(Shell scripts are new terrain for me, I probably made some faulty assumptions. Still, don’t have mercy.)

• This script looks reasonable. I'd just suggest that a makefile might be useful for this kind of thing too, especially if you want to make the copying conditional on timestamps. – 200_success May 18 '15 at 19:20

This is pretty nicely written. I have a couple of tips to improve it though.

Don't include trailing / in path variables

Although there's absolutely nothing wrong with this:

REPO_PATH="$PWD/" # ... OS="osx/" # ... BASH_DEST="${REPO_PATH}${OS}bash/"  In my experience, not including the trailing slash is typically more ergonomic, and sometimes even less error-prone: • You might have a function that's expecting the trailing slash, and some callers might forget to append it correctly, and there's no reasonable way to enforce at "compile time". • When I see concatenated path-like variables like $REPO_PATH${OS}bash I'm always suspicious if the caller added the slash or not. It makes me constantly look over my shoulders and re-check all uses. Consider this alternative: REPO_PATH=$PWD
# ...
OS=osx
# ...
BASH_DEST="$REPO_PATH/$OS/bash"


Here, I know that "$REPO_PATH/$OS/bash" will be definitely close to what I expect, and no need to worry about forgotten slashes anywhere. I also know it's a path, which is less obvious in the case of the original.

Don't execute the same thing repeatedly

When you detect the OS, the script might execute uname 3 times. It would be better to run it once and save in a variable.

Avoid expr

expr is archaic. Usually there are better solutions. You could also simplify the OS detection conditions using [[ instead:

uname=$(uname -s) # Detect OS (OS X, Linux or Windows) if [[$uname = Darwin ]]; then
echo "Detected system: OS X"
OS=osx
ST_DIR="$HOME/Library/Application Support/Sublime Text 3/Packages/User/" elif [[$uname == *Linux* ]]; then
echo "Detected system: Linux"
OS=linux
ST_DIR="$HOME/.config/sublime-text-3/Packages/User/" elif [[$uname == *MINGW32_NT* ]]; then
echo "Detected system: Windows"
OS=win
ST_DIR="$HOME/AppData/Roaming/Sublime Text 3/Packages/User/" NPM_DIR="$HOME/AppData/Roaming/npm/node_modules/npm/"
fi


Quote paths that may contain spaces

It's not obvious that these paths will never contain spaces: