fix: uninstall_confirm swallows Ctrl-D — the abort was silent #72

Merged
dan-claude-bot merged 1 commit from fix/uninstall-confirm-eof into main 2026-07-20 12:23:51 +00:00
3 changed files with 44 additions and 1 deletions

View file

@ -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

View file

@ -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
} }

View file

@ -1888,6 +1888,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.)