#!/bin/bash # ============================================================================= # machine-setup — writing files into somebody's home # ============================================================================= # # Definitions only, like the other lib/ files. # # ── The rule ── # # A setup script may create a config file. It may not silently replace one the # user wrote. The original did the second: `cp .tmux.conf $USER_HOME/` on every # run, over whatever was there, and five separate `cat >>` into .zshrc with no # guard — so a second pass duplicated the starship init, the nvim PATH, bun, deno # and the aliases. # # Both of those are the same mistake in different shapes: writing without looking # first. The two helpers here are the two safe shapes. [[ -n "${MACHINE_SETUP_FILES_LOADED:-}" ]] && return 0 MACHINE_SETUP_FILES_LOADED=1 # Put a config file in place, asking before it replaces one the user has. # # Three outcomes, and the caller can tell them apart by the return code: # # 0 installed — there was nothing there, or the user chose to replace # 1 identical — already exactly this, nothing done # 2 kept — the user chose to keep theirs # # When the file exists and differs, this ASKS rather than deciding. Silently # keeping theirs is safe but unhelpful — they never learn that a newer version # exists — and silently replacing it is how a setup script eats somebody's #configuration. So: keep, replace, or show the difference first, and a replaced file is # always kept beside the new one. # # NOTE for callers: 1 and 2 are outcomes, not failures — but they are still # non-zero, so calling this as a plain command under `set -e` ends the script # before the result can be read. Always capture it: # # install_config "$src" "$dest" "$user" && rc=0 || rc=$? install_config() { local src="$1" dest="$2" owner="$3" answer if [[ ! -f "$dest" ]]; then install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest" return 0 fi cmp -s "$src" "$dest" && return 1 echo "" warn "${dest} already exists here, and differs from the one this script ships." # Never replace a file the user has without being told to. An unattended run # answers "keep", because the alternative is destroying configuration nobody # was present to defend. if [[ "${ASSUME_YES:-}" == "1" ]] || [[ ! -t 0 ]]; then echo " keeping yours (nothing was asked, so nothing is replaced)" return 2 fi while true; do echo " [1] keep yours — nothing changes" echo " [2] use ours — yours is kept as ${dest}.before-machine-setup" echo " [3] show me the difference first" echo "" if ! read -rp " Which one? (1/2/3) [1]: " answer; then echo "" echo " keeping yours" return 2 fi case "${answer:-1}" in 1) echo " keeping yours" return 2 ;; 2) cp -a "$dest" "${dest}.before-machine-setup" install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest" ok "replaced — yours is at ${dest}.before-machine-setup" return 0 ;; 3) echo "" # yours on the left, ours on the right: - is what you would lose, # + is what you would gain. diff -u --label "yours: ${dest}" --label "ours: ${src}" "$dest" "$src" | page echo "" ;; *) warn "Pick 1, 2 or 3." ;; esac done } # Append a block to a file exactly once. # # The block is wrapped in markers naming what it is, so a second run recognises # its own work instead of adding it again — and so a human reading the file can # see which lines came from here and delete them as a unit. # # append_once ~/.zshrc bun <<'EOF' # export PATH="$HOME/.bun/bin:$PATH" # EOF # # Returns 0 if it wrote, 1 if the block was already there. # # One limitation, and it bites the author rather than the user: RENAMING a marker # orphans the block that used the old name. append_once only recognises the name # it is given, so the previous block stays in the file doing whatever it did. # Changing a block's CONTENT has the same shape — the marker is found, so the new # content is never written. Both need the old block removed by hand. append_once() { local file="$1" name="$2" local begin="# >>> machine-setup: ${name} >>>" local end="# <<< machine-setup: ${name} <<<" if [[ -f "$file" ]] && grep -qF "$begin" "$file"; then return 1 fi { echo "" echo "$begin" cat echo "$end" } >>"$file" }