2 Commits
4 changed files with 170 additions and 26 deletions

No files matched your search

+11 -3
View File
@@ -130,8 +130,13 @@ package (pacman backend with paru or yay, see below) and a space on every other
One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are
stable for the whole session (rows that leave the list leave a gap). Every token stable for the whole session (rows that leave the list leave a gap). Every token
of a line is validated first: a line with an unknown token or a number that is not of a line is checked for syntax first: an unknown token rejects the line. A number
listed prints an error, runs nothing, and the prompt comes back. or package name that is not listed prints an error and the remaining commands still run.
For `?` and `??`, a listed package name can replace the number, for example
`su-dummy-a?` or `su-dummy-a??`. A digits-only reference is interpreted as a number.
Names are restricted to these information commands to avoid unnecessary parsing
complexity: package names may contain `-` or end in `x`, which conflicts with the
other command suffixes.
| token | action | | token | action |
|-------|--------| |-------|--------|
@@ -147,7 +152,10 @@ listed prints an error, runs nothing, and the prompt comes back.
| `h`, `?` | help | | `h`, `?` | help |
| end of input | abort without upgrading (exit 2) | | end of input | abort without upgrading (exit 2) |
Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--all` Several tokens may share a line (`1? 3- 4+`); they run left to right. Consecutive
`N+` commands are processed together: selected packages required by another selected
package are recorded as dependencies, regardless of their order. `*+` works the same
way. With `--all`
trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the
session ends by itself (exit 0). session ends by itself (exit 0).
+10 -2
View File
@@ -125,6 +125,14 @@ Refreshing package lists... done.
``` ```
Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets): Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets):
Unknown tokens reject the line; nonexistent numbers report an error and are skipped.
For `?` and `??`, the exact name of a listed package may replace `N`; a digits-only
reference is treated as a number. Missing or no longer listed names are reported and
skipped. Other commands accept only numbers: package names can contain `-` or end in
`x`, and limiting name references to information commands avoids unnecessary parsing
complexity.
Consecutive `N+` commands and `*+` trust the selected dependency roots, recording
other selected packages in their closures as dependencies regardless of input order.
| token | action | | token | action |
|--------|---------------------------------------------------------------------------| |--------|---------------------------------------------------------------------------|
@@ -153,8 +161,8 @@ On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package
`pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`. `pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`.
Numbers are stable for the whole session (removed rows leave a gap, never renumber). Numbers are stable for the whole session (removed rows leave a gap, never renumber).
All tokens of a line are validated first: an unknown token or a number that is not listed Syntax is validated first; an unknown token rejects the line. Numbers that are not listed
prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without print an error and are skipped while the remaining commands run. EOF on stdin aborts without
upgrading (exit 2) so a piped run can never upgrade by accident unless it sends an empty upgrading (exit 2) so a piped run can never upgrade by accident unless it sends an empty
line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0). line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0).
+61 -19
View File
@@ -6,7 +6,7 @@
# and zypper: list what would be upgraded, interrogate every package, remember the # and zypper: list what would be upgraded, interrogate every package, remember the
# ones you trust, then run the native upgrade (with per-run exclusions). # ones you trust, then run the native upgrade (with per-run exclusions).
# #
# Version: 0.1.0 # Version: 0.3.0
# #
# Specification: docs/DESIGN.md. Single file, bash >= 4.3, no runtime dependency # Specification: docs/DESIGN.md. Single file, bash >= 4.3, no runtime dependency
# beyond the distro's package manager and coreutils/sed/grep (awk only on the apt # beyond the distro's package manager and coreutils/sed/grep (awk only on the apt
@@ -23,7 +23,7 @@ export LC_ALL=C DEBIAN_FRONTEND=noninteractive
# never read from or written to a file (no HISTFILE; `history -r`/`-w`/`-a` are never used). # never read from or written to a file (no HISTFILE; `history -r`/`-w`/`-a` are never used).
unset HISTFILE unset HISTFILE
readonly SU_VERSION=0.1.0 readonly SU_VERSION=0.3.0
readonly SU_PROG=skeptical-updater readonly SU_PROG=skeptical-updater
readonly SU_STATE_HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)' readonly SU_STATE_HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
readonly SU_PROMPT_HELP='<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever' readonly SU_PROMPT_HELP='<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
@@ -114,6 +114,7 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack
that depend on it - installed packages only that depend on it - installed packages only
N?? full native info: the installed record; lines starting with '*' come N?? full native info: the installed record; lines starting with '*' come
from the upstream candidate (differing values, fields it adds) from the upstream candidate (differing values, fields it adds)
For N? and N??, a listed package name may be used instead of N.
N- exclude from this run (toggle); shown with a '-' marker N- exclude from this run (toggle); shown with a '-' marker
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
when other installed packages depend on it, like pacman -Rns when other installed packages depend on it, like pacman -Rns
@@ -138,6 +139,7 @@ session_help() {
<enter> upgrade now, skipping the rows marked '-' <enter> upgrade now, skipping the rows marked '-'
N? what is it: description, install reason + installed packages that need it N? what is it: description, install reason + installed packages that need it
N?? full native info: installed record, '*' lines from the upstream candidate N?? full native info: installed record, '*' lines from the upstream candidate
A listed package name may replace N for ? and ?? (e.g. okular?).
N- exclude N from this run (toggle) N- exclude N from this run (toggle)
Nx purge N now: lists every package the purge removes, then asks y/N. Nx purge N now: lists every package the purge removes, then asks y/N.
x refuses when other installed packages depend on the target, like x refuses when other installed packages depend on the target, like
@@ -2712,9 +2714,28 @@ session_purge() {
} }
session_trust() { # session_trust IDX... session_trust() { # session_trust IDX...
local i now local i j d now covered
now=$(now_utc) local -a selected=() roots=()
local -A seen=() reach=()
for i in "$@"; do for i in "$@"; do
[[ -z ${seen[$i]+x} ]] || continue
seen[$i]=1
selected+=("$i")
done
for i in "${selected[@]}"; do
closure_of "${R_NAME[i]}"
for d in ${CLOSURE[@]+"${CLOSURE[@]}"}; do reach["$i,$d"]=1; done
done
for i in "${selected[@]}"; do
covered=0
for j in "${selected[@]}"; do
[[ $i != "$j" && -n ${reach[$j,${R_NAME[i]}]+x} ]] || continue
if [[ -z ${reach[$i,${R_NAME[j]}]+x} ]] || ((j < i)); then covered=1; break; fi
done
((covered)) || roots+=("$i")
done
now=$(now_utc)
for i in "${roots[@]}"; do
trust_pkg "${R_NAME[i]}" "$now" trust_pkg "${R_NAME[i]}" "$now"
R_EXCL[i]=0 R_EXCL[i]=0
done done
@@ -2747,24 +2768,19 @@ session_toggle_all() {
SESSION_PROCEED=0 SESSION_PROCEED=0
UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own
# session_line LINE: validate every token first (a typo never executes half a line), then run them
session_line() { session_line() {
local tok n op i local tok n op i name
local -a toks=() local -a toks=() batch=()
local -A initial=() named=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$' local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$'
local name_re='^([A-Za-z0-9_+@][A-Za-z0-9_+.@:~-]*)(\?\?|\?)$'
read -r -a toks <<<"$1" || true read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then if ((${#toks[@]} == 0)); then
SESSION_PROCEED=1 SESSION_PROCEED=1
return 0 return 0
fi fi
for tok in "${toks[@]}"; do for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then
n=$((10#${BASH_REMATCH[1]}))
if ! num_row "$n"; then
err "no package [$n] in the list (l to redraw)"
return 0
fi
else
case $tok in case $tok in
'*+' | '*-' | l | q | h | '?') ;; '*+' | '*-' | l | q | h | '?') ;;
*) *)
@@ -2774,15 +2790,26 @@ session_line() {
esac esac
fi fi
done done
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then
initial[$n]=$REPLY
named[${R_NAME[REPLY]}]=$REPLY
fi
done
for tok in "${toks[@]}"; do for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then if [[ $tok =~ $re ]]; then
n=$((10#${BASH_REMATCH[1]})) n=$((10#${BASH_REMATCH[1]}))
op=${BASH_REMATCH[2]} op=${BASH_REMATCH[2]}
if ! num_row "$n"; then if [[ -z ${initial[$n]+x} ]] || ((R_GONE[${initial[$n]:-0}])); then
err "package [$n] has left the list; ignoring the rest of the line" err "no package [$n] in the list (l to redraw)"
return 0 continue
fi fi
i=$REPLY i=${initial[$n]}
if [[ $op == + ]]; then
batch+=("$i")
continue
fi
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $op in case $op in
'?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; '?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
'??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; '??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
@@ -2802,9 +2829,23 @@ session_line() {
exit 0 exit 0
fi fi
;; ;;
+) session_trust "$i" ;;
esac esac
elif [[ $tok =~ $name_re ]]; then
name=${BASH_REMATCH[1]}
op=${BASH_REMATCH[2]}
if [[ -z ${named[$name]+x} ]] || ((R_GONE[${named[$name]:-0}])); then
err "no package '$name' in the list (l to redraw)"
continue
fi
i=${named[$name]}
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
if [[ $op == '?' ]]; then
print_info "${R_NAME[i]}" "${R_OLD[i]}" || true
else else
print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true
fi
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $tok in case $tok in
'*+') '*+')
local -a idx=() local -a idx=()
@@ -2823,6 +2864,7 @@ session_line() {
esac esac
fi fi
done done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
} }
cmd_interactive() { cmd_interactive() {
+88 -2
View File
@@ -101,6 +101,54 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
log_has 'apt-cache show --no-all-versions su-dummy-a' log_has 'apt-cache show --no-all-versions su-dummy-a'
} }
@test "listed package names support both information commands alongside numeric commands" {
run skup --no-refresh <<<$'su-dummy-a? 2? su-dummy-b?? 3-\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
assert_line ' Package: su-dummy-b'
assert_line 'excluded from this run: su-dummy-c'
log_has 'dpkg-query -s -- su-dummy-b'
}
@test "unknown or hidden package names report an error and do not stop later commands" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<$'missing? su-dummy-a?? 2?\nq'
assert_success
assert_output --partial "no package 'missing' in the list"
assert_output --partial "no package 'su-dummy-a' in the list"
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
}
@test "names work for trusted rows shown with --all and new dependencies" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh --all <<<$'su-dummy-a?\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
FAKE_APT_SIM=real run skup --no-refresh <<<$'su-dummy-newdep? su-dummy-newdep??\nq'
assert_success
assert_line 'reason: not installed'
log_lacks '^dpkg-query -s -- su-dummy-newdep'
log_has 'apt-cache show --no-all-versions su-dummy-newdep'
}
@test "a package removed during a line cannot be queried by name" {
run skup --no-refresh <<<$'3x su-dummy-c? 2?\ny\nq'
assert_success
assert_output --partial "no package 'su-dummy-c' in the list"
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
}
@test "package names are not accepted for trust, exclude or purge" {
for token in su-dummy-a+ su-dummy-a- su-dummy-ax; do
run skup --no-refresh <<<"$token"$'\nq'
assert_success
assert_output --partial "unknown command '$token'"
[[ ! -e $STATE_FILE ]]
log_lacks '^apt-get -y purge'
done
}
@test "N?? on a new dependency (old version -) never looks for an installed record" { @test "N?? on a new dependency (old version -) never looks for an installed record" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq' FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
assert_success assert_success
@@ -292,11 +340,49 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
run skup --no-refresh <<<$'*+\nl\nq' run skup --no-refresh <<<$'*+\nl\nq'
assert_success assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)' assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line 'trusted su-dummy-b'
assert_line 'trusted su-dummy-c' assert_line 'trusted su-dummy-c'
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:' assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
run cut -f2,3 "$STATE_FILE" run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-b${T}user" assert_line "su-dummy-b${T}dep:su-dummy-a"
}
@test "consecutive trust commands keep dependencies under their selected root in either order" {
for line in '1+ 2+ 3+' '2+ 3+ 1+'; do
rm -rf "$STATE"
run skup --no-refresh <<<"$line"$'\nq'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line 'trusted su-dummy-c'
refute_output --partial 'no package ['
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
assert_line "su-dummy-c${T}user"
done
}
@test "invalid numbers do not stop other commands on the line" {
run skup --no-refresh <<<$'9+ 2- 0? 3+\nq'
assert_success
assert_output --partial 'no package [9] in the list'
assert_output --partial 'no package [0] in the list'
assert_line 'excluded from this run: su-dummy-b'
assert_line 'trusted su-dummy-c'
}
@test "a package removed by a purge does not stop later commands" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2? 3-\ny\nq'
assert_success
assert_output --partial 'no package [2] in the list'
assert_line 'excluded from this run: su-dummy-c'
}
@test "trusting a dependency and its root with --all keeps the dependency implicit" {
run skup --no-refresh --all <<<$'2+ 1+\nq'
assert_success
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
} }
@test "*- excludes every listed package; *- again re-includes them" { @test "*- excludes every listed package; *- again re-includes them" {