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
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
listed prints an error, runs nothing, and the prompt comes back.
of a line is checked for syntax first: an unknown token rejects the line. A number
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 |
|-------|--------|
@@ -147,7 +152,10 @@ listed prints an error, runs nothing, and the prompt comes back.
| `h`, `?` | help |
| 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
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):
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 |
|--------|---------------------------------------------------------------------------|
@@ -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`.
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
prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without
Syntax is validated first; an unknown token rejects the line. Numbers that are not listed
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
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
# 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
# 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).
unset HISTFILE
readonly SU_VERSION=0.1.0
readonly SU_VERSION=0.3.0
readonly SU_PROG=skeptical-updater
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'
@@ -114,6 +114,7 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack
that depend on it - installed packages only
N?? full native info: the installed record; lines starting with '*' come
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
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
when other installed packages depend on it, like pacman -Rns
@@ -138,6 +139,7 @@ session_help() {
<enter> upgrade now, skipping the rows marked '-'
N? what is it: description, install reason + installed packages that need it
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)
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
@@ -2712,9 +2714,28 @@ session_purge() {
}
session_trust() { # session_trust IDX...
local i now
now=$(now_utc)
local i j d now covered
local -a selected=() roots=()
local -A seen=() reach=()
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"
R_EXCL[i]=0
done
@@ -2747,24 +2768,19 @@ session_toggle_all() {
SESSION_PROCEED=0
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() {
local tok n op i
local -a toks=()
local tok n op i name
local -a toks=() batch=()
local -A initial=() named=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$'
local name_re='^([A-Za-z0-9_+@][A-Za-z0-9_+.@:~-]*)(\?\?|\?)$'
read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then
SESSION_PROCEED=1
return 0
fi
for tok in "${toks[@]}"; do
if [[ $tok =~ $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
if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then
case $tok in
'*+' | '*-' | l | q | h | '?') ;;
*)
@@ -2774,15 +2790,26 @@ session_line() {
esac
fi
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
if [[ $tok =~ $re ]]; then
n=$((10#${BASH_REMATCH[1]}))
op=${BASH_REMATCH[2]}
if ! num_row "$n"; then
err "package [$n] has left the list; ignoring the rest of the line"
return 0
if [[ -z ${initial[$n]+x} ]] || ((R_GONE[${initial[$n]:-0}])); then
err "no package [$n] in the list (l to redraw)"
continue
fi
i=$REPLY
i=${initial[$n]}
if [[ $op == + ]]; then
batch+=("$i")
continue
fi
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $op in
'?') print_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
fi
;;
+) session_trust "$i" ;;
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
print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true
fi
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $tok in
'*+')
local -a idx=()
@@ -2823,6 +2864,7 @@ session_line() {
esac
fi
done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
}
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'
}
@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" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
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'
assert_success
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 '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
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" {