Sharing .zshrc Across Machines
I have a problem that I'm sure others have also faced. I have several different machines but I want my .zshrc to be the same for each of them. Not only this, but I also use Linux and macOS.
How do I Share the Config?
Git! This is the most obvious answer but I've made a git repo with just the .zshrc file inside it. I then added some configuration to the top of the file that instructs it on how to update itself from the repository:
# Config:
DOTFILES_REPO_URL="ssh://[email protected]:2222/alex.thomson/zshrc.git"
DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" # local clone path
DOTFILES_REMOTE="${DOTFILES_REMOTE:-origin}" # remote name
DOTFILES_BRANCH="${DOTFILES_BRANCH:-main}" # branch to track
DOTFILES_ZSHRC_PATH="${DOTFILES_ZSHRC_PATH:-.zshrc}" # path inside repo
_mktempdir() {
setopt localtraps
local d
d="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles.XXXXXX")" || return 1
trap 'rm -rf "$d"' EXIT
print -r -- "$d"
}
zshrc_update_now() {
[[ $- == *i* ]] || return 0
local tmpdir
tmpdir="$(_mktempdir)" || return 0
git clone --quiet --depth=1 --branch "$DOTFILES_BRANCH" \
"$DOTFILES_REPO_URL" "$tmpdir" || return 0
repo_zshrc="$tmpdir/$DOTFILES_ZSHRC_PATH"
[[ -f "$repo_zshrc" ]] || return 0
if cmp -s "$repo_zshrc" "$HOME/.zshrc"; then
return 0
fi
echo "~/.zshrc updated from dotfiles repo"
cp -p "$HOME/.zshrc" "$HOME/.zshrc.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
tmpfile="$(mktemp "$HOME/.zshrc.XXXXXX")" || return 1
cp "$repo_zshrc" "$tmpfile"
mv -f "$tmpfile" "$HOME/.zshrc"
echo "Reloading ~/.zshrc"
source "$HOME/.zshrc"
}This code works fine, but importantly there is nothing calling it yet. I don't want this code called every time I open up the terminal, so I instead opted to run it once per day when the terminal is opened. This is actually pretty simple:
ZSH_UPDATE_STAMP="${ZSH_UPDATE_STAMP:-$HOME/.cache/zshrc-selfupdate.stamp}"
zshrc_self_update_daily() {
[[ $- == *i* ]] || return 0
mkdir -p "${ZSH_UPDATE_STAMP:h}" 2>/dev/null
local today last
today="$(date +%Y-%m-%d)"
last="$(cat "$ZSH_UPDATE_STAMP" 2>/dev/null)"
[[ "$last" == "$today" ]] && return 0
print -r -- "$today" >| "$ZSH_UPDATE_STAMP"
zshrc_update_now
}
zshrc_self_update_daily || true
With this final piece of code, my .zshrc can now self-update itself each day, keeping my machines up-to-date and synchronized; but there's still a problem: how do I push changes?
Pushing Changes
I decided to add a function for editing my .zshrc. Since I've installed Neovim on each of my machine I decided to make the function open the configuration with nvim, then once editing was complete, check for changes, push, and source ~/.zshrc again:
zshrc_push_via_temp_clone() {
# Validate config:
[[ -n "${DOTFILES_REPO_URL:-}" ]] || { echo "DOTFILES_REPO_URL is not set"; return 1; }
[[ -n "${DOTFILES_BRANCH:-}" ]] || { echo "DOTFILES_BRANCH is not set"; return 1; }
[[ -n "${DOTFILES_ZSHRC_PATH:-}" ]] || { echo "DOTFILES_ZSHRC_PATH is empty (set to .zshrc)"; return 1; }
# Disallow absolute paths (e.g., /etc/...) inside the repo:
if [[ "$DOTFILES_ZSHRC_PATH" == /* ]]; then
echo "DOTFILES_ZSHRC_PATH must be a path inside the repo (not absolute): $DOTFILES_ZSHRC_PATH"
return 1
fi
local tmpdir
tmpdir="$(_mktempdir)" || return 0
# Fresh clone of the branch we will push to:
command git clone --quiet --depth=1 --branch "$DOTFILES_BRANCH" \
"$DOTFILES_REPO_URL" "$tmpdir" || return 1
repo_zshrc="$tmpdir/$DOTFILES_ZSHRC_PATH"
mkdir -p "${repo_zshrc:h}" || return 1
# Copy this zshrc into the repo clone:
command cp "$HOME/.zshrc" "$repo_zshrc" || return 1
# Nothing changed vs repo? Bail.
if command git -C "$tmpdir" diff --quiet -- "$DOTFILES_ZSHRC_PATH"; then
echo "No dotfiles changes to push."
return 0
fi
# Perform syntax check before committing:
command zsh -n "$repo_zshrc" || {
echo "Refusing to push: zsh syntax check failed."
return 1
}
command git -C "$tmpdir" add -- "$DOTFILES_ZSHRC_PATH" || return 1
msg="Update .zshrc ($(hostname -s 2>/dev/null || hostname), $(date +%Y-%m-%d))"
command git -C "$tmpdir" commit -m "$msg" --quiet || return 1
command git -C "$tmpdir" push --quiet origin "$DOTFILES_BRANCH" || return 1
echo "Pushed .zshrc to $DOTFILES_BRANCH"
}
# Opens an editor for this file, then pushes changes to git.
zshrc_edit() {
local before after
before="$(command shasum -a 256 "$HOME/.zshrc" 2>/dev/null | awk '{print $1}')"
"${EDITOR:-nvim}" "$HOME/.zshrc"
local rc=$?
[[ $rc -eq 0 ]] || { echo "Editor exited non-zero; not pushing."; return $rc; }
after="$(command shasum -a 256 "$HOME/.zshrc" 2>/dev/null | awk '{print $1}')"
[[ -n "$before" && "$before" == "$after" ]] && {
echo "~/.zshrc unchanged."
return 0
}
# Push changes (if any) using a temp clone:
zshrc_push_via_temp_clone || {
echo "Push failed; leaving local ~/.zshrc as-is."
return 1
}
# Reload after successful push:
source "$HOME/.zshrc"
}Now when ever I want to edit my .zshrc, I can just use that function and it will be replicated onto my other machines the next time I open a shell (providing I haven't already opened one that day). If I really want to get the latest changes, I can always manually call the zshrc_update_now function.