Created
October 17, 2025 14:43
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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