feat(prompt): readline line editing with per-session history

When stdin is a terminal the prompt is read with bash's readline,
`IFS= read -e -r -p '> ' REPLY`, so the arrow keys no longer print escape
garbage: Left/Right, Home/End, Ctrl-A/E, Backspace/Delete edit the line,
and Up/Down walk the lines entered in this session (`history -s` for every
line with a non-blank character; empty lines are not recorded). Down past
the newest entry restores the line typed before Up, and Up at the oldest
entry or Down on the new line ring the bell, as readline does by default.
The Nx y/N answer is read the same way and never recorded.

Nothing persists: HISTFILE is unset at startup and the history is never
read or written. Verified on bash 4.3.48 and 5.2.15 that `read -e` and
`history -s` share one history list in a non-interactive shell, that
inherited HISTSIZE/HISTCONTROL/HISTIGNORE have no effect there, and that
Ctrl-C at the prompt restores the terminal. readline draws the prompt on
stderr, as bash does. Piped stdin keeps the plain `read -r` exactly,
including the echo of each line and EOF => exit 2.

New unit tests (tests/unit/lineedit.bats) drive a real pty with script(1):
history recall, the bell at both ends, the stashed line, Left-arrow
editing, the y/N answer kept out of the history, no history file under
HOME/TMPDIR, ^D => exit 2. The E2E suite gets a pty smoke test (Up
recalls 1?), skipped where the image has no script(1).
This commit is contained in:
bryanpedini committed 2026-10-10 14:01:38 +02:00
1 parent 71d7f87aa3
commit 296c77e319
7 files changed
+183 -11

No files matched your search

+17
View File
@@ -207,6 +207,23 @@ pacman_unpublish() {
repo-add --quiet su-dummy.db.tar.gz ./su-dummy-a-2.0-1-any.pkg.tar.zst ./su-dummy-b-2.0-1-any.pkg.tar.zst ./su-dummy-c-2.0-1-any.pkg.tar.zst)
}
# pty_session INPUT ARGS...: run skeptical-updater ARGS on a pseudo-terminal (script(1) of
# util-linux) with INPUT typed in one go (readline consumes it as it reads: no timing involved);
# sets status, output and lines like `run` (without the pty's CRs; NO_COLOR as stdout is a terminal)
# and PTY_RAW, the raw transcript. Skips the test when the image has no script(1).
# shellcheck disable=SC2034 # status/lines are read by bats-assert
pty_session() {
local input=$1 cmd
shift
command -v script >/dev/null 2>&1 || skip "script(1) (util-linux) not available in this image: no pty to drive"
PTY_RAW=$BATS_TEST_TMPDIR/pty.raw
printf -v cmd '%q ' env TERM=dumb NO_COLOR=1 skeptical-updater "$@"
status=0
printf '%s' "$input" | timeout 120 script -qfec "$cmd" /dev/null >"$PTY_RAW" 2>&1 || status=$?
output=$(tr -d '\r' <"$PTY_RAW")
mapfile -t lines <<<"$output"
}
# session_start ARGS...: run skeptical-updater ARGS in the background with a FIFO as stdin and wait
# for its prompt; sets SESSION_PID and SESSION_FD (write the input to it; fd 3 belongs to bats)
session_start() {
+8
View File
@@ -219,6 +219,14 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl
[[ ! -e $STATE_FILE ]]
}
@test "on a terminal, Up recalls the previous line (readline, history of this session only)" {
pty_session $'1?\r\e[A\rq\r' --no-refresh
assert_success
[[ $(grep -c '^su-dummy-a: ' <<<"$output") -eq 2 ]] || fail "1? should have run twice (Up + Enter)"
assert_line 'Quit: nothing was upgraded.'
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
}
@test "--dry-run: enter prints the native upgrade command and changes nothing" {
before=$(holds_snapshot)
run skeptical-updater --dry-run <<<''