diff options
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 |
