Skip to content

Instantly share code, notes, and snippets.

@dzogrim
Created October 17, 2025 14:43
Show Gist options
  • Save dzogrim/f158a971bf25c69aaa5a6eb47840660f to your computer and use it in GitHub Desktop.
Save dzogrim/f158a971bf25c69aaa5a6eb47840660f to your computer and use it in GitHub Desktop.
Compare local config files/folders against two Dropbox references and show concise diffs/missing files
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025 dzogrim
# SPDX-FileContributor: dzogrim <[email protected]>
# ==============================================================================
# Compare local config files/folders against two Dropbox references
# ("work" and "personal") and show concise diffs/missing files.
# ==============================================================================
# How it works
# • The REFSETS array lists triplets: "current|ref_work|ref_personal".
# • The current runtime "environment" is inferred from $LOGNAME
# • For each triplet:
# 1) Select the primary reference based on the environment,
# and the other becomes the secondary.
# 2) For each file in "current":
# - If the reference is a folder, compare matching basenames.
# - If the reference is a file, compare that file directly.
# - Print a short unified diff (context 1) when content differs.
# - Report when a file is missing from the reference.
#
# A special handling for git user
# • When comparing ".gitconfig", the whole `[user]` section is ignored
# (from `[user]` up to the next section header) on both sides so that
# personal identity settings do not trigger false positives.
#
# Dependencies:
# • `jq`, `gdiff`, `gawk` and Dropbox desktop for macOS
#
# Configuration
# • REFSETS: add lines for new (current|work|personal) sets.
# • BLACKLIST: basenames to skip (supports shell globs).
# • LOGNAME: environment mapping is defined in the case/esac below.
#
# Notes
# • Adjust REFSETS and BLACKLIST to your setup.
# • Adjust LOGNAME to your environments.
# • The script is read-only: it never mutates files, only compares.
# • Designed to be concise in output for regular audits.
# ==============================================================================
set -euo pipefail
# Resolve Dropbox personal root from Dropbox info.json
dropboxDir="$(jq -r '.personal.path' <"${HOME}/.dropbox/info.json")"
# REFSETS: triplets "current|ref_work|ref_personal"
REFSETS=(
"${HOME}/.bashrc.d|${dropboxDir}/Private/_SyncThat/.dotfiles_work/.bashrc.d|${dropboxDir}/Private/_SyncThat/.dotfiles/.bashrc.d"
"${HOME}/.config/nix|${dropboxDir}/Private/_SyncThat/.dotfiles_work/.config/nix|${dropboxDir}/Private/_SyncThat/.dotfiles/.config/nix"
)
# Repositories to audit at a single-file level (not as directories)
REFSETS+=(
"${HOME}/.gitconfig|${dropboxDir}/Private/_SyncThat/.dotfiles_work/.gitconfig|${dropboxDir}/Private/_SyncThat/.dotfiles/.gitconfig"
"${HOME}/.gitignore_global|${dropboxDir}/Private/_SyncThat/.dotfiles_work/.gitignore_global|${dropboxDir}/Private/_SyncThat/.dotfiles/.gitignore_global"
)
# Files to ignore (shell globs allowed)
BLACKLIST=(
'flake.lock'
'registry.json'
)
# Environment detection from $LOGNAME → sets primary/secondary
case "${LOGNAME}" in
user.name) current_env="pro" ;;
nickname) current_env="perso" ;;
*) echo "❌ Environment not recognized for user ${LOGNAME}"; exit 1 ;;
esac
# is_blacklisted <basename>
# Returns 0 if the file should be skipped, 1 otherwise
is_blacklisted() {
local name="$1"
for pat in "${BLACKLIST[@]}"; do
[[ "$name" == "$pat" ]] && return 0
done
return 1
}
# ------------------------------------------------------------------------------
# audit_against <current_path> <reference_path> <label>
# Compares each file in <current_path> against <reference_path>
# ------------------------------------------------------------------------------
audit_against() {
local current="$1" reference="$2" label="$3"
# 1) Build the list of files from "current"
local -a files=()
if [[ -f "$current" ]]; then
files=("$current")
elif [[ -d "$current" ]]; then
mapfile -t files < <(find "$current" -maxdepth 1 -type f ! -name '.DS_Store')
else
echo "❌ Path not found: $current"
return
fi
# 2) Compare each file against its reference (directory OR file)
for f in "${files[@]}"; do
local base ref_file
base="$(basename "$f")"
is_blacklisted "$base" && continue
if [[ -d "$reference" ]]; then
# référence = directory
ref_file="${reference%/}/${base}"
else
# reference = file
ref_file="$reference"
fi
if [[ -f "$ref_file" ]]; then
if [[ "$base" == ".gitconfig" ]]; then
# Ignore the [user] section when comparing .gitconfig
if ! diff -q \
<(awk 'BEGIN{s=0} /^\[user\]/{s=1; next} /^\[/{s=0} s==0{print}' "$f") \
<(awk 'BEGIN{s=0} /^\[user\]/{s=1; next} /^\[/{s=0} s==0{print}' "$ref_file") >/dev/null; then
echo -e "\n⚠️ ${base} differs from ${label} backup:"
{ diff -u -1 \
<(awk 'BEGIN{s=0} /^\[user\]/{s=1; next} /^\[/{s=0} s==0{print}' "$ref_file") \
<(awk 'BEGIN{s=0} /^\[user\]/{s=1; next} /^\[/{s=0} s==0{print}' "$f") || true; } | sed 's/^/ /'
fi
else
if ! diff -q "$f" "$ref_file" >/dev/null; then
echo -e "\n⚠️ ${base} differs from ${label} backup:"
{ diff -u -1 "$ref_file" "$f" || true; } | sed 's/^/ /'
fi
fi
else
echo -e "\n❌ ${base} missing from ${label} backup"
fi
done
}
# Main loop over REFSETS
for entry in "${REFSETS[@]}"; do
IFS='|' read -r current ref_pro ref_perso <<<"${entry}"
# Select primary/secondary based on environment
if [[ "${current_env}" == "pro" ]]; then
primary="${ref_pro}"
secondary="${ref_perso}"
secondary_label="perso"
else
primary="${ref_perso}"
secondary="${ref_pro}"
secondary_label="pro"
fi
# Readable section header for output
printf "\n============================================\n"
printf "%-15s %s\n" "🔎 Auditing from:" "${current}"
printf "%-15s %s\n" " Environment:" "${current_env}"
printf "%-15s %s\n" " ❯ from:" "${primary}"
printf "%-15s %s\n\n" " ➜ against:" "${secondary}"
# Audits
audit_against "${current}" "${primary}" "${current_env}"
audit_against "${current}" "${secondary}" "${secondary_label}"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment