test: widen the read-guard sweep to bin/ and to plain-statement reads #82
3 changed files with 68 additions and 1 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -199,6 +199,21 @@ on the way to cutting its first release, and this file starts there.
|
||||||
cannot reopen quietly — including via a symlinked directory, which
|
cannot reopen quietly — including via a symlinked directory, which
|
||||||
`globstar` declines to traverse.
|
`globstar` declines to traverse.
|
||||||
|
|
||||||
|
- **Ctrl-D at the `rig uninstall` confirm no longer aborts in silence**
|
||||||
|
(#68) — `uninstall_confirm`'s `read -r reply` was unguarded. Under
|
||||||
|
`set -euo pipefail`, and called as a plain statement, EOF made `read`
|
||||||
|
return non-zero and killed the shell *at the read* — the `case` on the
|
||||||
|
next line never ran, so `die "aborted."` never fired. The operator saw
|
||||||
|
the question, pressed Ctrl-D, and got nothing back: no message, just
|
||||||
|
exit 1, at the exact moment the tool had asked whether to delete their
|
||||||
|
install. It failed closed (nothing was ever removed), but nothing said
|
||||||
|
so. Now `read -r reply || reply=""`, so EOF falls through to the `*)`
|
||||||
|
arm and aborts out loud — the spelling `commands/db.sh` already used for
|
||||||
|
the same `[y/N]` shape, one file away. `test/cli.sh` gains the first
|
||||||
|
drills of the interactive path, driving `y` and Ctrl-D through a real
|
||||||
|
pty (util-linux `script`, skipped where it is absent) and asserting the
|
||||||
|
MESSAGE rather than the exit code, which the bug also produced.
|
||||||
|
|
||||||
## 0.2.0 — 2026-07-19
|
## 0.2.0 — 2026-07-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
7
bin/rig
7
bin/rig
|
|
@ -241,7 +241,12 @@ uninstall_confirm() { # $1 = question
|
||||||
fi
|
fi
|
||||||
local reply
|
local reply
|
||||||
printf 'rig: %s? [y/N] ' "$1"
|
printf 'rig: %s? [y/N] ' "$1"
|
||||||
read -r reply
|
# `|| reply=""` is load-bearing under `set -e`: Ctrl-D makes read return
|
||||||
|
# non-zero, and as a plain-statement call site this function would die HERE,
|
||||||
|
# before the case — the abort would be real but completely silent. Empty
|
||||||
|
# falls through to the `*)` arm, so EOF aborts with exactly one message,
|
||||||
|
# the same spelling commands/db.sh uses for the same [y/N] shape.
|
||||||
|
read -r reply || reply=""
|
||||||
case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac
|
case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
47
test/cli.sh
47
test/cli.sh
|
|
@ -817,6 +817,30 @@ check "runner remove: headless token prompt refuses loudly" 0 "" \
|
||||||
# so a bare `read -rsp` (no `||` on its line) must not exist anywhere.
|
# so a bare `read -rsp` (no `||` on its line) must not exist anywhere.
|
||||||
check "prompts: no bare read -rsp remains" 1 "" \
|
check "prompts: no bare read -rsp remains" 1 "" \
|
||||||
grep -RE 'read -rsp[^|]*$' "$ROOT/commands/"
|
grep -RE 'read -rsp[^|]*$' "$ROOT/commands/"
|
||||||
|
# ...and the same sweep, widened on the two axes #68 escaped through (#75).
|
||||||
|
# That check reads `-rsp` literally and scans commands/ only; #68 was a plain
|
||||||
|
# `read -r reply` in bin/rig, so it missed on the spelling AND on the path.
|
||||||
|
# The class is the shape, not the flags: any `read` run as a PLAIN STATEMENT
|
||||||
|
# under `set -euo pipefail` kills the shell at EOF, before the `case` that
|
||||||
|
# would have printed the abort — silently, with exit 1, indistinguishable
|
||||||
|
# from a normal refusal.
|
||||||
|
#
|
||||||
|
# So: match `read` at the start of a statement (leading whitespace only),
|
||||||
|
# whatever its flags or arity, then subtract the two shapes that are safe by
|
||||||
|
# construction:
|
||||||
|
# `||` — the guard itself (`|| die`, `|| reply=""`, `|| { echo; die … }`).
|
||||||
|
# An errexit-exempt read, which is the whole cure.
|
||||||
|
# `<<<` — a here-string always supplies a terminating newline, so the read
|
||||||
|
# cannot return non-zero. lib/users-config.sh:50/:78 are these.
|
||||||
|
# `while`/`until`/`if` heads need no subtraction: the anchor already excludes
|
||||||
|
# them, since `read` is not the first word on those lines. Keep the guard on
|
||||||
|
# the read's own line — a `\`-continued `||` reads as unguarded here, by
|
||||||
|
# design, because it is not visible at the point of failure.
|
||||||
|
unguarded_read() {
|
||||||
|
grep -REn '^[[:space:]]*read[[:space:]]' "$ROOT/bin/" "$ROOT/commands/" \
|
||||||
|
| grep -Ev '\|\||<<<'
|
||||||
|
}
|
||||||
|
check "prompts: no unguarded plain-statement read remains (#75)" 1 "" unguarded_read
|
||||||
|
|
||||||
# --- runner install: --repo must agree with what the box is already on -------
|
# --- runner install: --repo must agree with what the box is already on -------
|
||||||
# The bug: `install --repo B` on a box registered to repo A skipped configure,
|
# The bug: `install --repo B` on a box registered to repo A skipped configure,
|
||||||
|
|
@ -1888,6 +1912,29 @@ check "uninstall --all: RIG_YES=1 is consent without a terminal" 0 "uninstalled"
|
||||||
check "uninstall --all: ZERO residue — root and symlinks" 0 "" bash -c "
|
check "uninstall --all: ZERO residue — root and symlinks" 0 "" bash -c "
|
||||||
[ ! -e '$H2' ] && [ ! -L '$H2' ] &&
|
[ ! -e '$H2' ] && [ ! -L '$H2' ] &&
|
||||||
[ ! -e '$B2/rig' ] && [ ! -L '$B2/rig' ]"
|
[ ! -e '$B2/rig' ] && [ ! -L '$B2/rig' ]"
|
||||||
|
# --- the INTERACTIVE confirm, driven through a real pty (#68) ---------------
|
||||||
|
# uninstall_confirm only reaches its `read` when stdin is a terminal, which is
|
||||||
|
# why every check above goes through --force or RIG_YES and why the EOF bug
|
||||||
|
# survived. `script` gives us the terminal. Assert on the MESSAGE, never on the
|
||||||
|
# exit code: the unfixed `read -r reply` (no `|| reply=""`) dies at the read
|
||||||
|
# under `set -e` and also exits 1, just silently — an exit-code assertion is
|
||||||
|
# green against the bug and proves nothing.
|
||||||
|
if command -v script >/dev/null 2>&1; then
|
||||||
|
H8="$WORK/h8"; B8="$WORK/b8"
|
||||||
|
inst "$H8" "$B8" >/dev/null 2>&1
|
||||||
|
check "uninstall: Ctrl-D at the confirm prompt ABORTS OUT LOUD (#68)" 1 "aborted." \
|
||||||
|
irig bash -c "script -qec \"'$B8/rig' uninstall --all\" /dev/null </dev/null"
|
||||||
|
check "uninstall: ...and the EOF abort removed nothing" 0 "" \
|
||||||
|
bash -c "[ -e '$H8' ] && [ -e '$B8/rig' ]"
|
||||||
|
printf 'y\n' > "$WORK/yes-in"
|
||||||
|
check "uninstall: 'y' at the confirm prompt goes through" 0 "uninstalled" \
|
||||||
|
irig bash -c "script -qec \"'$B8/rig' uninstall --all\" /dev/null < '$WORK/yes-in'"
|
||||||
|
check "uninstall: ...and that really removed the install" 0 "" \
|
||||||
|
bash -c "[ ! -e '$H8' ] && [ ! -e '$B8/rig' ]"
|
||||||
|
else
|
||||||
|
echo "skip: interactive uninstall confirm drills (no util-linux script)"
|
||||||
|
fi
|
||||||
|
|
||||||
# The last word is a re-check: a survivor must turn into a loud INCOMPLETE,
|
# The last word is a re-check: a survivor must turn into a loud INCOMPLETE,
|
||||||
# never a cheerful "uninstalled". (Root ignores file modes, so this drill is
|
# never a cheerful "uninstalled". (Root ignores file modes, so this drill is
|
||||||
# meaningful — and runnable — for a non-root runner only.)
|
# meaningful — and runnable — for a non-root runner only.)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue