testsAndMisc/meta/scripts/verify_shell_split.sh
Krzysztof kuhy Rudnicki dbc4d099a3 Commit the shell-split verifier and the traps it found
meta/scripts/verify_shell_split.sh was a scratchpad file; it has caught
five bugs across 18 splits that bash -n and shellcheck both passed on,
so it belongs in the repo. Tested in both directions - it fails on a
deliberately broken source line and passes once restored.

docs/shell-split-recipes.md gains what the last two batches taught:
splitting a file twice can displace the first cut's source line, globals
read across a seam have three different fixes depending on how many, and
unguarded cd shows up constantly because a moved function finally gets
linted on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:59:20 +02:00

67 lines
2.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verify a shell script split into lib/*.sh: syntax, lint, libs reachable.
#
# Usage: verify_shell_split.sh <script> <function> [function...]
#
# The stubbed run is the part that matters. bash -n and shellcheck both pass on
# a script whose `source` line resolves to nothing, so this mirrors the repo
# tree into a temp dir, replaces the final `main "$@"` with a probe that only
# checks the named functions are defined, and runs that. Nothing else executes,
# which is what makes it safe for installers and enforcement scripts.
#
# Across 18 splits it caught five bugs no static check did: SCRIPT_DIR computed
# from $0, a redeclared readonly SCRIPT_DIR, a source line displaced by a second
# split, and two libs sourcing a sibling. See docs/shell-split-recipes.md.
#
# Only works when the script ends in `main "$@"` or `main`. Scripts that run
# top-level statements need the by-hand checks in that doc instead.
set -euo pipefail
script="$1"
shift
required_funcs=("$@")
dir="$(dirname "$script")"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
echo "--- $(basename "$script")"
bash -n "$script" || exit 1
shellcheck -x "$script" || exit 1
for lib in "$dir"/lib/*.sh; do
[[ -e $lib ]] || continue
bash -n "$lib" || exit 1
shellcheck "$lib" || exit 1
done
dollar='$'
grep -q "source \"${dollar}SCRIPT_DIR/" "$script" || {
echo "no source line found" >&2
exit 1
}
# Mirror the repo tree so repo-relative sources (../../lib/common.sh) resolve.
repo="$(git -C "$dir" rev-parse --show-toplevel)"
rel="$(realpath --relative-to="$repo" "$script")"
mkdir -p "$tmp/$(dirname "$rel")"
cp -r "$repo/linux_configuration/scripts/lib" "$tmp/linux_configuration/scripts/" 2>/dev/null || true
cp "$script" "$tmp/$rel"
[[ -d "$dir/lib" ]] && cp -r "$dir/lib" "$tmp/$(dirname "$rel")/"
entry="$tmp/$rel"
probe='for f in'
for f in "${required_funcs[@]}"; do probe+=" $f"; done
probe+="; do declare -F \"${dollar}f\" >/dev/null || { echo \"MISSING: ${dollar}f\"; exit 1; }; done"
probe+='; echo " libs sourced, functions defined"'
python3 - "$entry" "$probe" <<'PY'
import sys
from pathlib import Path
p, probe = Path(sys.argv[1]), sys.argv[2]
out = [probe + "\n" if l.rstrip() in ('main "$@"', "main") else l
for l in p.read_text().splitlines(keepends=True)]
p.write_text("".join(out))
PY
bash "$entry"