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:
1 parent
71d7f87aa3
commit
296c77e319
7 files changed
+183
-11
No files matched your search
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Line editing and the per-session history of the prompt when stdin is a terminal: the applet runs
|
||||
# on a real pty (script(1)) and gets the keys as escape sequences - Up \e[A, Down \e[B, Right \e[C,
|
||||
# Left \e[D, Enter \r - typed in one go. apt backend, fixture rows [1] su-dummy-a [2] su-dummy-b
|
||||
# [3] su-dummy-c. The piped (non-TTY) prompt is covered by interactive.bats.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
UP=$'\e[A' DOWN=$'\e[B' LEFT=$'\e[D' CR=$'\r'
|
||||
DESC_A='su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
||||
|
||||
@test "pty: Up recalls the previous line, Enter runs it again" {
|
||||
pty_skup "1?${CR}${UP}${CR}q${CR}" --no-refresh
|
||||
assert_success
|
||||
[[ $(count_lines "$DESC_A") -eq 2 ]] || fail "1? should have run twice"
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
[[ $(pty_bells) -eq 0 ]] || fail "unexpected bell"
|
||||
}
|
||||
|
||||
@test "pty: Up at the oldest entry rings the bell and stays there" {
|
||||
pty_skup "1?${CR}${UP}${UP}${CR}q${CR}" --no-refresh
|
||||
assert_success
|
||||
[[ $(pty_bells) -ge 1 ]] || fail "no BEL in the pty output"
|
||||
[[ $(count_lines "$DESC_A") -eq 2 ]] || fail "the oldest entry (1?) should have run again"
|
||||
}
|
||||
|
||||
@test "pty: a line typed before Up is stashed and comes back with Down (readline's saved line)" {
|
||||
pty_skup "1?${CR}3-${UP}${DOWN}${CR}${CR}" --no-refresh
|
||||
assert_success
|
||||
assert_line 'excluded from this run: su-dummy-c'
|
||||
[[ $(count_lines "$DESC_A") -eq 1 ]] || fail "the recalled 1? must not have run"
|
||||
assert_line 'Excluded from this run: su-dummy-c'
|
||||
log_has 'apt-mark hold su-dummy-c'
|
||||
[[ $(pty_bells) -eq 0 ]] || fail "unexpected bell"
|
||||
}
|
||||
|
||||
@test "pty: Down on the new line rings the bell, also once the stashed line is back" {
|
||||
pty_skup "1?${CR}${DOWN}q${CR}" --no-refresh
|
||||
assert_success
|
||||
[[ $(pty_bells) -ge 1 ]] || fail "no BEL for Down on the new line"
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
|
||||
pty_skup "1?${CR}3-${UP}${DOWN}${DOWN}${CR}q${CR}" --no-refresh
|
||||
assert_success
|
||||
[[ $(pty_bells) -ge 1 ]] || fail "no BEL for Down on the restored line"
|
||||
assert_line 'excluded from this run: su-dummy-c'
|
||||
}
|
||||
|
||||
@test "pty: Left moves the cursor: 2? Left Left 1 Enter is the command 12?" {
|
||||
pty_skup "2?${LEFT}${LEFT}1${CR}q${CR}" --no-refresh
|
||||
assert_success
|
||||
assert_output --partial 'no package [12] in the list (l to redraw)'
|
||||
refute_output --partial 'su-dummy-b: dummy package'
|
||||
}
|
||||
|
||||
@test "pty: empty lines and the y/N answer stay out of the history" {
|
||||
# history: 3x only - Up shows 3x, a second Up rings the bell (y was not added); 3x again is
|
||||
# rejected because [3] was purged
|
||||
pty_skup "3x${CR}y${CR}${UP}${UP}${CR}q${CR}" --no-refresh
|
||||
assert_success
|
||||
assert_line 'purged: su-dummy-c'
|
||||
assert_output --partial 'no package [3] in the list (l to redraw)'
|
||||
[[ $(pty_bells) -ge 1 ]] || fail "y must not be in the history"
|
||||
log_has 'apt-get -y purge su-dummy-c'
|
||||
}
|
||||
|
||||
@test "pty: the history is never written to or read from a file (HISTFILE is ignored)" {
|
||||
export HOME=$BATS_TEST_TMPDIR/home TMPDIR=$BATS_TEST_TMPDIR/tmp
|
||||
mkdir -p "$HOME" "$TMPDIR"
|
||||
export HISTFILE=$HOME/.bash_history HISTCONTROL=ignoredups HISTSIZE=0
|
||||
pty_skup "1?${CR}${UP}${CR}${UP}${CR}${CR}" --no-refresh
|
||||
assert_success
|
||||
[[ $(count_lines "$DESC_A") -eq 3 ]] || fail "the history did not work (inherited HISTSIZE/HISTCONTROL?)"
|
||||
assert_line 'Upgrade finished.'
|
||||
run find "$HOME" "$TMPDIR" -mindepth 1
|
||||
assert_success
|
||||
assert_output ''
|
||||
}
|
||||
|
||||
@test "pty: end of input (^D on an empty line) still aborts with exit 2" {
|
||||
pty_skup "1-${CR}"$'\x04' --no-refresh
|
||||
assert_failure 2
|
||||
assert_line 'excluded from this run: su-dummy-a'
|
||||
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
Reference in new issue
Block a user