diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-08 20:22:12 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-08 20:22:12 +0200 |
| commit | 6ca8744933a273acc0a8f446ae7b9cffdbb700bd (patch) | |
| tree | 0a47296df1766322de6c7eaf12dd84dcc8a0c8d9 /nvtable | |
| download | danix-nvchecker-6ca8744933a273acc0a8f446ae7b9cffdbb700bd.tar.gz danix-nvchecker-6ca8744933a273acc0a8f446ae7b9cffdbb700bd.zip | |
Initial commit: unified nvchecker config and nvtable
Merge a personal nvchecker config with the .extras/nvchecker.toml of three
SlackBuild repositories into a single deduplicated config, and add nvtable,
which joins nvchecker's upstream results against the version installed on
this host and the version shipped by the packaging repo.
The stanza name is the Slackware package name wherever a package exists, so
results join directly against /var/log/packages and <pkg>/<pkg>.info; nvtable
carries a small alias map for the cases that genuinely differ.
Installed versions are resolved from /var/log/packages, the appimage-updater
state file, and flatpak. Caught-up packages are recorded with nvtake under an
flock, so old_ver tracks this system rather than only upstream history.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'nvtable')
| -rwxr-xr-x | nvtable | 272 |
1 files changed, 272 insertions, 0 deletions
@@ -0,0 +1,272 @@ +#!/bin/bash +# nvtable - compare upstream (nvchecker) vs installed vs shipped-in-repo versions. +# +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# Three version sources per tracked name: +# upstream - nvchecker new_ver.json +# installed - /var/log/packages, then appimage-updater state, then flatpak +# repo - VERSION= in <repo>/<pkg>/<pkg>.info +# +# When installed == upstream the row is caught up, and nvtake records it so +# old_ver tracks the system. See take_caught_up() for the locking. + +set -uo pipefail + +CONF="${NVTABLE_CONF:-$HOME/.config/nvchecker/nvchecker.toml}" +NEWVER="${NVTABLE_NEWVER:-$HOME/.config/nvchecker/new_ver.json}" +AI_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/appimage-updater/installed.json" +LOCK="${TMPDIR:-/tmp}/nvtable-$UID.lock" + +REPOS=( + "sbo:$HOME/Programming/GIT/sbo-slackbuilds" + "my:$HOME/Programming/GIT/GITHUB/my-slackbuilds" + "sps:$HOME/Programming/GIT/GITHUB/Slackware-Pentesting-Suite" +) + +# Stanza name -> installed package name(s), space separated for one-to-many. +# Only for names that differ; everything else joins directly. +declare -A ALIAS=( + [kvantum]="kvantum-qt5 kvantum-qt6" + [metasploit-framework]="metasploit-framework-bin" +) + +# Stanza name -> SlackBuild directory name, where the repo dir differs from the +# stanza (a -bin build tracked under its plain upstream name). +declare -A REPO_ALIAS=( + [kitty]="kitty-bin" + [claude-code]="claude-code-bin" + [kvantum]="kvantum-qt6" + [metasploit-framework]="metasploit-framework-bin" +) + +SHOW_ALL=0; NO_TAKE=0; NO_COLOR=0; NO_EMOJI=0 +for a in "$@"; do + case "$a" in + -a|--all) SHOW_ALL=1 ;; + -n|--no-take) NO_TAKE=1 ;; + -C|--no-color) NO_COLOR=1 ;; + -E|--no-emoji) NO_EMOJI=1 ;; + -h|--help) + cat <<EOF +usage: nvtable [-a] [-n] [-C] [-E] + -a, --all show every tracked package, not just those needing action + -n, --no-take do not run nvtake on caught-up packages + -C, --no-color plain output + -E, --no-emoji ASCII status markers instead of emoji +EOF + exit 0 ;; + *) echo "nvtable: unknown option $a" >&2; exit 2 ;; + esac +done + +[[ -t 1 ]] || NO_COLOR=1 +if (( NO_COLOR )); then + R=""; B=""; DIM=""; RED=""; GRN=""; YEL=""; CYA=""; WHT="" +else + R=$'\e[0m'; B=$'\e[1m'; DIM=$'\e[2m'; RED=$'\e[1;31m' + GRN=$'\e[1;32m'; YEL=$'\e[1;33m'; CYA=$'\e[1;36m'; WHT=$'\e[1;37m' +fi + +# Status markers. Emoji are double-width, so each ASCII fallback is padded to +# two columns to keep the name column aligned either way. +if (( NO_EMOJI )); then + E_UP="!!" E_REPO="~ " E_OK="ok" E_NA="- " +else + E_UP="🔴" E_REPO="🟡" E_OK="🟢" E_NA="⚪" +fi + +command -v jq >/dev/null || { echo "nvtable: jq required" >&2; exit 1; } +[[ -r $NEWVER ]] || { echo "nvtable: no $NEWVER (run nvchecker first)" >&2; exit 1; } + +# --- installed versions ----------------------------------------------------- +# Package files are PRGNAM-VERSION-ARCH-BUILD, but VERSION may itself contain a +# dash (solvespace-3.1-659215d), so the PRGNAM/VERSION boundary cannot be found +# by counting dashes. Drop ARCH-BUILD from the right, then keep the whole +# remainder; lookup_installed() anchors on the name it is searching for. +declare -A PKGREST +while IFS= read -r f; do + rest=${f%-*} # drop BUILD + rest=${rest%-*} # drop ARCH -> "PRGNAM-VERSION" + [[ -n $rest ]] && PKGREST[$rest]=1 +done < <(/bin/ls -1 /var/log/packages 2>/dev/null) + +declare -A INST +for rest in "${!PKGREST[@]}"; do + # Default split: last dash. Correct for the common case, fixed up below for + # any name that a tracked stanza claims explicitly. + INST[${rest%-*}]=${rest##*-} +done + +# Given an exact package name, take everything after it as the version. The +# match is anchored on "<want>-" and strips exactly that many characters, so a +# VERSION containing dashes survives and a longer sibling package (kvantum-qt5 +# when asked for kvantum) is never mistaken for a match. +pkg_version() { + local want=$1 rest + for rest in "${!PKGREST[@]}"; do + if [[ ${rest,,} == "${want,,}-"* ]]; then + rest=${rest:${#want}+1} + # Reject when what follows is not a version (another name component). + [[ $rest == [0-9]* ]] || continue + echo "$rest"; return 0 + fi + done + return 1 +} + +# AppImages tracked by appimage-updater (its state is authoritative for those). +if [[ -r $AI_STATE ]]; then + while IFS=$'\t' read -r k v; do + [[ -n $k ]] && INST[$k]=$v + done < <(jq -r 'to_entries[]|"\(.key)\t\(.value.version)"' "$AI_STATE" 2>/dev/null) +fi + +# Flatpak apps, keyed on the last dot-component of the app id. +if command -v flatpak >/dev/null 2>&1; then + while IFS=$'\t' read -r app v; do + [[ -n $app ]] || continue + INST[${app##*.}]=$v + done < <(flatpak list --app --columns=application,version 2>/dev/null) +fi + +installed_of() { # echoes "version" or "" ; handles alias + case-insensitive + local n=$1 cand v + for cand in ${ALIAS[$n]:-$n}; do + # Anchored lookup first: correct even when VERSION contains a dash. + v=$(pkg_version "$cand") && { echo "$v"; return; } + [[ -n ${INST[$cand]:-} ]] && { echo "${INST[$cand]}"; return; } + for k in "${!INST[@]}"; do + [[ ${k,,} == "${cand,,}" ]] && { echo "${INST[$k]}"; return; } + done + done +} + +# --- repo .info versions ---------------------------------------------------- +declare -A RVER RWHICH +for entry in "${REPOS[@]}"; do + tag=${entry%%:*}; dir=${entry#*:} + [[ -d $dir ]] || continue + while IFS= read -r inf; do + pkg=$(basename "$inf" .info) + v=$(sed -n 's/^VERSION="\(.*\)"$/\1/p' "$inf" | head -1) + [[ -n $v ]] || continue + if [[ -n ${RVER[$pkg]:-} ]]; then + RWHICH[$pkg]="${RWHICH[$pkg]},$tag" + else + RVER[$pkg]=$v; RWHICH[$pkg]=$tag + fi + done < <(find "$dir" -mindepth 2 -maxdepth 3 -name '*.info' 2>/dev/null) +done + +# --- compare ---------------------------------------------------------------- +# Versions come from unrelated sources, so normalise before comparing: strip a +# leading v, fold - and _ to dots, lowercase. +norm() { local s=${1,,}; s=${s#v}; s=${s//[-_]/.}; echo "$s"; } + +# Some packages append build metadata the upstream version never carries: +# a kernel version (r8125 9.018.00_6.18.41) or a git hash (solvespace +# 3.1-659215d). Treat installed as equal when it merely extends upstream at a +# component boundary. +vers_equal() { + local a b; a=$(norm "$1"); b=$(norm "$2") + [[ $a == "$b" ]] && return 0 + [[ -n $a && -n $b && $b == "$a".* ]] && return 0 + return 1 +} + +rows=(); take=(); n_up=0; n_repo=0; n_ok=0; n_na=0 +while IFS=$'\t' read -r name uver; do + [[ -n $name ]] || continue + iver=$(installed_of "$name") + rkey=$name + if [[ -z ${RVER[$rkey]:-} ]]; then + for cand in ${REPO_ALIAS[$name]:-} ${ALIAS[$name]:-}; do + [[ -n ${RVER[$cand]:-} ]] && { rkey=$cand; break; } + done + fi + rver=${RVER[$rkey]:-}; rwhich=${RWHICH[$rkey]:-} + + same_inst=0; [[ -n $iver ]] && vers_equal "$uver" "$iver" && same_inst=1 + same_repo=1; [[ -n $rver ]] && { vers_equal "$uver" "$rver" || same_repo=0; } + + if (( same_inst )) && (( same_repo )); then + (( n_ok++ )); take+=("$name") + (( SHOW_ALL )) || continue + else + (( same_inst )) || { [[ -n $iver ]] && (( n_up++ )); } + (( same_repo )) || (( n_repo++ )) + # Nothing installed and the .info already matches upstream: there is no + # action to take on this host, so keep it out of the default view. + if [[ -z $iver ]] && (( same_repo )); then + (( n_na++ )) + (( SHOW_ALL )) || continue + fi + fi + # Packed with | not tab: read collapses runs of whitespace IFS chars, which + # would shift every field left whenever installed or repo is empty. + rows+=("$name|$uver|$iver|$rver|$rwhich|$same_inst|$same_repo") +done < <(jq -r '.data|to_entries[]|"\(.key)\t\(.value.version // .value)"' "$NEWVER") + +# --- output ----------------------------------------------------------------- +if ((${#rows[@]})); then + # Widen the name column to the longest actual name, so a long stanza does not + # shunt every later column out of alignment. + w=22 + for r in "${rows[@]}"; do n=${r%%|*}; (( ${#n} > w )) && w=${#n}; done + printf '%s %-*s %-14s %-14s %s%s\n' "$DIM" "$w" "package" "upstream" "installed" "repo" "$R" + for r in "${rows[@]}"; do + IFS='|' read -r name uver iver rver rwhich si sr <<<"$r" + if [[ -z $iver ]]; then ic="$DIM"; ishow="--" + elif (( si )); then ic="$GRN"; ishow=$iver + else ic="$RED"; ishow=$iver + fi + # Installed-behind and repo-behind are independent, and a row can be both + # (upstream moved, and neither the system nor the .info has caught up), so + # the marker shows each dimension rather than collapsing to the worst one. + if [[ -z $iver ]]; then mark="$E_NA" + elif (( si )); then mark="$E_OK" + else mark="$E_UP" + fi + (( sr )) || mark+="$E_REPO" + if [[ -z $rver ]]; then rc="$DIM"; rtxt="--" + elif (( sr )); then rc="$GRN"; rtxt="$rwhich $rver" + else rc="$YEL"; rtxt="$rwhich $rver" + fi + # Pad the marker cell to a fixed width: rows carrying both markers must not + # push the name column right relative to rows carrying one. + if (( NO_EMOJI )); then mcell=$(printf '%-5s' "$mark") + else mcell=$mark; (( ${#mark} > 1 )) || mcell="$mark " + fi + printf ' %s %s%-*s%s %-14s %s%-14s%s %s%s%s\n' \ + "$mcell" "$WHT" "$w" "$name" "$R" "$uver" "$ic" "$ishow" "$R" "$rc" "$rtxt" "$R" + done + echo +fi +printf ' %s%d%s to upgrade · %s%d%s repo%s to bump · %s%d%s current' \ + "$RED" "$n_up" "$R" "$YEL" "$n_repo" "$R" "$( ((n_repo==1)) || echo s )" \ + "$GRN" "$n_ok" "$R" +(( n_na )) && printf ' · %s%d%s not installed' "$DIM" "$n_na" "$R" +echo + +# --- nvtake ----------------------------------------------------------------- +# Record caught-up packages so old_ver reflects the system. flock serialises +# concurrent logins: nvtake rewrites old_ver.json without locking of its own. +take_caught_up() { + ((${#take[@]})) || return 0 + command -v nvtake >/dev/null || return 0 + exec 9>"$LOCK" || return 0 + flock -n 9 || return 0 # another shell is taking; skip, next run catches it + nvtake -c "$CONF" --ignore-nonexistent "${take[@]}" >/dev/null 2>&1 + exec 9>&- +} +(( NO_TAKE )) || take_caught_up |
