diff --git a/README.md b/README.md index e903f53..e50baaa 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,12 @@ 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 checked for syntax first: an unknown token rejects the line. A number -that is not listed prints an error and the remaining commands still run. +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 | |-------|--------| diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 6505d56..93f6945 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -126,6 +126,11 @@ 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. diff --git a/skeptical-updater b/skeptical-updater index 81a2eb3..8f6cda8 100755 --- a/skeptical-updater +++ b/skeptical-updater @@ -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.2.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.2.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=' 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() { 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 @@ -2767,17 +2769,18 @@ SESSION_PROCEED=0 UPGRADE_FAILED_MSG= # set by _upgrade when the failing step is not the package manager's own session_line() { - local tok n op i + local tok n op i name local -a toks=() batch=() - local -A initial=() + 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 + if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then case $tok in '*+' | '*-' | l | q | h | '?') ;; *) @@ -2788,7 +2791,10 @@ session_line() { fi done for ((n = 1; n <= N_MAX; n++)); do - if num_row "$n"; then initial[$n]=$REPLY; fi + if num_row "$n"; then + initial[$n]=$REPLY + named[${R_NAME[REPLY]}]=$REPLY + fi done for tok in "${toks[@]}"; do if [[ $tok =~ $re ]]; then @@ -2824,6 +2830,20 @@ session_line() { fi ;; 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 diff --git a/tests/unit/interactive.bats b/tests/unit/interactive.bats index 463cc62..1654ac1 100644 --- a/tests/unit/interactive.bats +++ b/tests/unit/interactive.bats @@ -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