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?
- Checks whether the command is executed from within the root of a git repository
- Detects which OS it’s operating on (Windows, Linux or OS X) and saves that information in a variable
OS - Assigns variables for certain tool groups (like Bash, Git, etc.) and creates the target directories in the repo if they don’t exist
- 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.)
-
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– 200_success2015年05月18日 19:20:47 +00:00Commented May 18, 2015 at 19:20
1 Answer 1
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}bashI'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.
-
\$\begingroup\$ Very nice, thank you, janos. I also added an if-statement for the NPM and ST copy commands to check whether
$NPM_DIRand$ST_DIRexist. \$\endgroup\$user35408– user354082015年05月17日 13:44:56 +00:00Commented May 17, 2015 at 13:44