mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-13 23:40:48 +01:00
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
This commit is contained in:
parent
7e967de79b
commit
dbc4d099a3
@ -11,10 +11,13 @@ enforcement script that mutates the live system or the phone.**
|
||||
- After each split, `bash -n` and `shellcheck` the entry script _and_ every new
|
||||
lib. **Neither catches a `source` line that resolves to the wrong path** —
|
||||
both passed clean on a split whose lib was unreachable at runtime.
|
||||
- So the real check is a **stubbed run**: copy the entry script and its libs to
|
||||
a `mktemp -d`, `sed` the final `main` invocation into a `declare -F` probe,
|
||||
and run it. That executes the `source` lines and nothing else. This caught
|
||||
the bug below; no static check did.
|
||||
- So the real check is a **stubbed run**, scripted as
|
||||
`meta/scripts/verify_shell_split.sh <script> <function>...`: it mirrors the
|
||||
repo tree into a `mktemp -d`, replaces the final `main` invocation with a
|
||||
`declare -F` probe, and runs that. The `source` lines execute and nothing
|
||||
else, which makes it safe for installers. Across 18 splits it caught five
|
||||
bugs no static check did. It needs the script to end in `main "$@"`; for the
|
||||
rest, use the by-hand checks below.
|
||||
|
||||
### Use `${BASH_SOURCE[0]}`, not `$0`
|
||||
|
||||
@ -95,6 +98,37 @@ grep -rn -E '^\s*(sudo )?(install|cp)\s.*(SCRIPT_DIR|REPO).*/usr/local/' \
|
||||
Reuse it instead of adding a second declaration, or the script dies with
|
||||
`readonly variable`. `bash -n` and `shellcheck` both pass on that.
|
||||
|
||||
### Splitting the same file twice is the risky move
|
||||
|
||||
The splitter inserts its `source` line relative to the file as it currently
|
||||
stands, so a second pass can **displace the first pass's line**. On
|
||||
`20-dump-stock.sh` that left `verify_dump` called but never defined, and the
|
||||
repair then put the line inside the lib rather than the entry script.
|
||||
|
||||
After any double split: `grep -n 'source "\$SCRIPT_DIR' <entry> <libs>` and
|
||||
check that the entry sources **every** lib and **no lib sources another**.
|
||||
`shellcheck` without `-x` on a lib is what surfaces this — a lib sourcing a
|
||||
sibling reports SC1091 "does not exist", because the relative path is only
|
||||
valid from the entry script's directory.
|
||||
|
||||
### Globals read across the seam
|
||||
|
||||
Common, and the fix depends on how many:
|
||||
|
||||
- **One or two, module-level constants** — rename to UPPERCASE, matching what
|
||||
the surrounding files already do (`QUIET`, `FORCE`). Worked for
|
||||
`DESKTOP_DIR` in `fix_unity.sh`.
|
||||
- **A flag the caller computes** — pass it as a parameter. Worked for
|
||||
`persist_with_systemd_logind`.
|
||||
- **Several, threaded through three files** — stop. See below.
|
||||
|
||||
### Unguarded `cd` turns up constantly
|
||||
|
||||
Two found in five splits (`build_website`, the study-material generator), both
|
||||
pre-existing, both a real bug: a failed `cd` runs the next command in the
|
||||
caller's directory. They only surface on splitting because a moved function
|
||||
gets linted in isolation. Fix with `|| die` / `|| return 1`, never a suppression.
|
||||
|
||||
### When to give up on a seam
|
||||
|
||||
`clean_audio.sh` was reverted. Two probe-result globals were read by functions
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"intent": "Commit the shell-split verifier as a repo script instead of leaving it in a scratchpad, since it has caught five bugs that bash -n and shellcheck both missed.",
|
||||
"scope": [
|
||||
"meta/scripts/verify_shell_split.sh (new)",
|
||||
"docs/shell-split-recipes.md (points at it, and records the double-split, cross-seam-global and unguarded-cd findings)",
|
||||
"Non-goal: no behaviour change to any script it checks"
|
||||
],
|
||||
"changes": [
|
||||
"Added meta/scripts/verify_shell_split.sh. It mirrors the repo tree into a mktemp -d, copies the entry script and its lib/, replaces the final main invocation with a declare -F probe, and runs that. The source lines execute and nothing else, which is what makes it usable on installers and enforcement scripts under decision 6.",
|
||||
"Its header records what it caught across 18 splits: SCRIPT_DIR computed from $0, a redeclared readonly SCRIPT_DIR, a source line displaced by a second split, and two libs sourcing a sibling. It also states its limit - the script must end in main \"$@\".",
|
||||
"Resolved two SC2016 warnings by building the literal dollar sign through a variable rather than adding a disable directive, so the file is clean with no suppression.",
|
||||
"docs/shell-split-recipes.md now points at the script and adds three sections earned in the last two batches: splitting a file twice, globals read across the seam, and unguarded cd."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash meta/scripts/verify_shell_split.sh <a correctly split script>",
|
||||
"result": "pass",
|
||||
"evidence": "install_joplin.sh reports 'libs sourced, functions defined'."
|
||||
},
|
||||
{
|
||||
"command": "deliberately point the source line at a nonexistent lib, re-run, then restore",
|
||||
"result": "pass",
|
||||
"evidence": "Reports 'lib/DOES_NOT_EXIST.sh: No such file or directory' and exits non-zero; passes again once restored. Tested in both directions, because a checker that never fails is worse than none."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck meta/scripts/verify_shell_split.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Clean with no disable directive."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed>",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck, shfmt and the shebang-executable hook all pass."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The verifier only works on scripts ending in `main \"$@\"` or `main`. Scripts that run top-level statements silently do less: the probe replaces nothing and the script would run for real, so those must use the by-hand checks in the doc instead. The header says so, but it is not enforced.",
|
||||
"It mirrors only linux_configuration/scripts/lib and the script's own lib/. A script sourcing something outside those paths would fail in the temp tree for a reason unrelated to the split."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the script disappears and the doc loses its pointer.",
|
||||
"The scratchpad copy is session-local and not a durable fallback."
|
||||
]
|
||||
}
|
||||
66
meta/scripts/verify_shell_split.sh
Executable file
66
meta/scripts/verify_shell_split.sh
Executable file
@ -0,0 +1,66 @@
|
||||
#!/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"
|
||||
Loading…
Reference in New Issue
Block a user