4
\$\begingroup\$

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.)

\$\endgroup\$
  • 2
    \$\begingroup\$ 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. \$\endgroup\$ – 200_success May 18 '15 at 19:20
6
\$\begingroup\$

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:

declare -a destinations=($BASH_DEST $GIT_DEST $NPM_DEST $RUBY_DEST $ST_DEST)

It's better to be safe, and put double-quotes around each.

More careful error handling

If you couldn't detect the OS, I suggest to print a warning and exit.

Use case for switches

The final OS-specific operations could be written a bit cleaner using a case statement.

\$\endgroup\$
  • \$\begingroup\$ Very nice, thank you, janos. I also added an if-statement for the NPM and ST copy commands to check whether $NPM_DIR and $ST_DIR exist. \$\endgroup\$ – kleinfreund May 17 '15 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.