From da9f1fbfdef99cd796779864f7ad234e742b235e Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 7 Jun 2026 21:43:33 -0500 Subject: [PATCH 01/69] Standardized printing. Organized color resources. Changed custom entry to accept multiple commands. Added systemd and fail2ban status. Added network usage by namespace. Added disk usage by pattern matching. Added G_TRUNCATE_MID. --- dietpi/func/dietpi-banner | 716 ++++++++++++++++++++++++++++++++----- dietpi/func/dietpi-globals | 32 +- 2 files changed, 651 insertions(+), 97 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2da3f34cfb..338e4ac69b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -5,6 +5,7 @@ # #//////////////////////////////////// # Created by Daniel Knight / daniel.knight@dietpi.com / dietpi.com + # Updated May 2026 by Tim Olson / timjolson@users.noreply.github.com # #//////////////////////////////////// # @@ -39,9 +40,92 @@ # Globals #///////////////////////////////////////////////////////////////////////////////////// readonly FP_SAVEFILE='/boot/dietpi/.dietpi-banner' - readonly FP_CUSTOM='/boot/dietpi/.dietpi-banner_custom' readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' + # ---------------------------------------------------------------------------- + # Custom preferences initialisation helper (can be called by external scripts after + # sourcing this file). It loads the user-saved settings file + # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. + # Usage: DIETPI_BANNER_LOAD_PREFS + # ---------------------------------------------------------------------------- + DIETPI_BANNER_LOAD_PREFS() + { + declare -g COLOUR_RESET='\e[0m' + declare -gA aCOLOUR=( + [0]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [1]='\e[1m' # Bold white | Main descriptions + [2]='\e[90m' # Grey | Subdued text + [3]='\e[91m' # Red | Update notifications / Bad state + [4]='\e[1;32m' # Green | Good state + [5]='\e[1;33m' # Yellow | Warning state + [6]='\e[1;36m' # Blue | Dynamic match + + [DIETPI_GREEN]='\e[38;5;154m' + [GREY]='\e[90m' + [RED]='\e[91m' + [GREEN]='\e[1;32m' + [YELLOW]='\e[1;33m' + [BLUE]='\e[1;36m' + [GOLD]='\e[38;5;220m' + [PURPLE]='\e[38;5;141m' + [VIVID_RED]='\e[38;5;196m' + [CYAN]='\e[38;5;45m' + + [RESET]='\e[0m' + [BOLD]='\e[1m' + [DIM]='\e[2m' + [UNDERLINE]='\e[4m' + [INVERSE]='\e[7m' + [BLINK]='\e[5m' + [HIDDEN]='\e[8m' + + [FG_BLACK]='\e[30m' + [FG_RED]='\e[31m' + [FG_GREEN]='\e[32m' + [FG_YELLOW]='\e[33m' + [FG_BLUE]='\e[34m' + [FG_MAGENTA]='\e[35m' + [FG_CYAN]='\e[36m' + [FG_WHITE]='\e[37m' + + [BRIGHT_BLACK]='\e[90m' + [BRIGHT_RED]='\e[91m' + [BRIGHT_GREEN]='\e[92m' + [BRIGHT_YELLOW]='\e[93m' + [BRIGHT_BLUE]='\e[94m' + [BRIGHT_MAGENTA]='\e[95m' + [BRIGHT_CYAN]='\e[96m' + [BRIGHT_WHITE]='\e[97m' + + [BG_BLACK]='\e[40m' + [BG_RED]='\e[41m' + [BG_GREEN]='\e[42m' + [BG_YELLOW]='\e[43m' + [BG_BLUE]='\e[44m' + [BG_MAGENTA]='\e[45m' + [BG_CYAN]='\e[46m' + [BG_WHITE]='\e[47m' + [BG_DARK_GREY]='\e[48;5;236m' + + [BG_BRIGHT_BLACK]='\e[100m' + [BG_BRIGHT_RED]='\e[101m' + [BG_BRIGHT_GREEN]='\e[102m' + [BG_BRIGHT_YELLOW]='\e[103m' + [BG_BRIGHT_BLUE]='\e[104m' + [BG_BRIGHT_MAGENTA]='\e[105m' + [BG_BRIGHT_CYAN]='\e[106m' + [BG_BRIGHT_WHITE]='\e[107m' + + [TRUE_GREEN]='\e[38;2;46;204;113m' + [TRUE_BG]='\e[48;2;44;62;80m' + + ) + + # Load settings here, to have chosen ${aCOLOUR[N]} applied to below strings + # shellcheck disable=SC1090 + [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + } + aDESCRIPTION=( 'Device model' @@ -54,7 +138,7 @@ 'Disk usage (RootFS)' 'Disk usage (userdata)' 'Weather (wttr.in)' - 'Custom banner entry' + 'Custom commands' 'Display DietPi useful commands?' 'MOTD' 'VPN status' @@ -65,36 +149,45 @@ 'Load average' 'Word-wrap lines on small screens' 'Kernel' + 'Network Usage' + 'Disk Usage' + 'Systemd Status' + 'Fail2Ban Status' ) # Set defaults: Disable CPU temp by default in VMs if (( $G_HW_MODEL == 20 )) then - aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0) + aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) else - aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0) + aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) fi - COLOUR_RESET='\e[0m' - aCOLOUR=( - - '\e[38;5;154m' # DietPi green | Lines, bullets and separators - '\e[1m' # Bold white | Main descriptions - '\e[90m' # Grey | Credits - '\e[91m' # Red | Update notifications - ) - # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Load settings here, to have chosen ${aCOLOUR[0]} applied to below strings - # shellcheck disable=SC1090 - [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + # Default custom command example + CUSTOM_COMMANDS[0]='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + # Default disk patterns + SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") - GREEN_LINE=" ${aCOLOUR[0]}─────────────────────────────────────────────────────$COLOUR_RESET" - GREEN_BULLET=" ${aCOLOUR[0]}-$COLOUR_RESET" - GREEN_SEPARATOR="${aCOLOUR[0]}:$COLOUR_RESET" + # Load user preferences, overriding the above defaults as needed + DIETPI_BANNER_LOAD_PREFS + + # Put loaded colors into the used slots + aCOLOUR[DIETPI_GREEN]=${aCOLOUR[0]} + aCOLOUR[WHITE]=${aCOLOUR[1]} + aCOLOUR[GREY]=${aCOLOUR[2]} + aCOLOUR[RED]=${aCOLOUR[3]} + aCOLOUR[GREEN]=${aCOLOUR[4]} + aCOLOUR[YELLOW]=${aCOLOUR[5]} + aCOLOUR[BLUE]=${aCOLOUR[6]} + + # Derived convenience strings + declare -g GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" + declare -g GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" + declare -g GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] @@ -154,17 +247,36 @@ Save() { - # Custom entry description - echo "aDESCRIPTION[10]='${aDESCRIPTION[10]}'" - for i in "${!aDESCRIPTION[@]}" do echo "aENABLED[$i]=${aENABLED[$i]}" done - for i in "${!aCOLOUR[@]}" + # Persist only the primary colour slots (0..6). Convert any actual + # ESC bytes to the two-character sequence \e so the saved file is + # portable when sourced in different shells. + for i in {0..6} do - echo "aCOLOUR[$i]='${aCOLOUR[$i]}'" + val="${aCOLOUR[$i]}" + # Replace actual ESC bytes (0x1b) with literal \e + esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') + # Escape single quotes for safe embedding + esc=${esc//\'/\\\'} + echo "aCOLOUR[$i]='$esc'" + done + + # Persist SPACE_BASE_PATTERNS (indices 0..N) + for i in "${!SPACE_BASE_PATTERNS[@]}"; do + patt=${SPACE_BASE_PATTERNS[$i]} + patt=${patt//\'/\\\'} + printf "SPACE_BASE_PATTERNS[%s]='%s'\n" "$i" "$patt" + done + + # Persist CUSTOM_COMMANDS (indices 0..N) + for i in "${!CUSTOM_COMMANDS[@]}"; do + cmd=${CUSTOM_COMMANDS[$i]} + cmd=${cmd//\'/\\\'} + printf "CUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" done echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" @@ -176,57 +288,96 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[3]}Update available" + local text_update_available_date="${aCOLOUR[YELLOW]}Update available" # New DietPi live patches available? elif Check_DietPi_Live_Patches then - local text_update_available_date="${aCOLOUR[3]}New live patches available" + local text_update_available_date="${aCOLOUR[YELLOW]}New live patches available" # APT update available? elif Check_APT_Updates then - local text_update_available_date="${aCOLOUR[3]}$PACKAGE_COUNT APT updates available" + local text_update_available_date="${aCOLOUR[YELLOW]}$PACKAGE_COUNT APT updates available" # Reboot required to finalise kernel upgrade? elif Check_Reboot then - local text_update_available_date="${aCOLOUR[3]}Reboot required" + local text_update_available_date="${aCOLOUR[RED]}Reboot required" else local locale=$(sed -n '/^[[:blank:]]*AUTO_SETUP_LOCALE=/{s/^[^=]*=//p;q}' /boot/dietpi.txt) local text_update_available_date=$(LC_ALL=${locale:-C.UTF-8} date '+%R - %a %x') fi echo -e "$GREEN_LINE - ${aCOLOUR[1]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET + ${aCOLOUR[WHITE]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET $GREEN_LINE" } - Print_Local_Ip() + Print_Subheader() + { + # ON THE CURRENT LINE Output green line separator with subheader title "$1" + echo -e "\r$GREEN_LINE + ${aCOLOUR[WHITE]}$1 $GREEN_SEPARATOR" + } + + Get_Local_Ip() { (( ${aENABLED[5]} )) || return 0 local iface=$(G_GET_NET -q iface) local ip=$(G_GET_NET -q ip) - echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[5]} $GREEN_SEPARATOR ${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" + echo "${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" } - Print_Cert_Status() + Get_Cert_Status() { - # Let's Encrypt cert status - MUST be run as root - local i - for i in /etc/letsencrypt/live/*/cert.pem - do - # shellcheck disable=SC2292 - [ -f "$i" ] || continue - openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' - return 0 - done - echo 'No certificate found' + Get_Cert_Status_Raw() + { + # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. + # also is `dash` compatible + local i + for i in /etc/letsencrypt/live/*/cert.pem + do + # shellcheck disable=SC2292 + [ -f "$i" ] || continue + openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' + return 0 + done + return 1 + } + + stat="" + code=0 + if [[ "$(id -u)" = "0" ]] + then + # Running as root + stat=$(Get_Cert_Status_Raw 2>/dev/null) + code=$? + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + stat=$(sudo -n dash -c "$(declare -f Get_Cert_Status_Raw); Get_Cert_Status_Raw 2>&1" 2> /dev/null) + code=$? + fi + + case $code in + 0) + case "$stat" in + Valid*) stat="${aCOLOUR[GREY]}$stat${COLOUR_RESET}" ;; + No\ certificate*) stat="${aCOLOUR[RED]}$stat${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[YELLOW]}$stat${COLOUR_RESET}" ;; + esac + ;; # stat is valid + 1) stat="${aCOLOUR[RED]}No certificate found${COLOUR_RESET}" ;; + 2) stat="${aCOLOUR[YELLOW]}NOPASSWD sudo required${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[YELLOW]}Unable to obtain cert status${COLOUR_RESET}" ;; + esac + + echo "$stat" } Print_Credits() { - echo -e " ${aCOLOUR[2]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + echo -e " ${aCOLOUR[GREY]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info @@ -241,32 +392,330 @@ $GREEN_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - echo -e " ${aCOLOUR[1]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" # New DietPi live patches available? elif (( $LIVE_PATCHES )) then - echo -e " ${aCOLOUR[1]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" # APT updates available? elif (( $PACKAGE_COUNT )) then - echo -e " ${aCOLOUR[1]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" # Reboot required to finalise kernel upgrade? elif (( $REBOOT_REQUIRED )) then - echo -e " ${aCOLOUR[1]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[RED]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" fi } Print_Useful_Commands() { - echo -e " ${aCOLOUR[1]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[1]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[1]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation - ${aCOLOUR[1]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor - ${aCOLOUR[1]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[WHITE]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[WHITE]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation + ${aCOLOUR[WHITE]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor + ${aCOLOUR[WHITE]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + } + + Print_Item_State() + { + # Print a single line item with green bullet, description "$1", green separator and state "$2" + # state should contain color and format codes as needed + local subtitle="$1" + local state="$2" + local width="${3:-}" + local color="${aCOLOUR[WHITE]}" + printf "%b%b %-${width}b %b %b\n" \ + "$GREEN_BULLET" "${color}" "$subtitle" "$GREEN_SEPARATOR" "$state" + } + + Get_Systemd_Status() + { + # Print systemd overall status based on "systemctl --failed" + FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2>/dev/null || true) + if [[ -z "$FAILED_CNT" || "$FAILED_CNT" -eq 0 ]]; then + state="${aCOLOUR[GREY]}No Services Failed${COLOUR_RESET}" + else + state="${aCOLOUR[RED]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + fi + echo "$state" + } + + Get_Fail2Ban_Status() + { + Get_Fail2Ban_Status_Raw() + { + # # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. + # # also is `dash` compatible + + # Count unique banned IPs via iptables/nftables compatibility. Try iptables first, + # fall back to nft if available. + COUNTIPS=0 + if command -v iptables >/dev/null 2>&1; then + COUNTIPS=$(iptables -L -n 2>/dev/null | awk '/REJECT/ {print $0}' | uniq -c | wc -l) + elif command -v nft >/dev/null 2>&1; then + COUNTIPS=$(nft list ruleset 2>/dev/null | awk '/reject/ {print $0}' | uniq -c | wc -l) + else + return 1 + fi + echo "$COUNTIPS" + return 0 + } + + COUNTIPS=0 + if [[ "$(id -u)" = "0" ]] + then + COUNTIPS="$(Get_Fail2Ban_Status_Raw 2>/dev/null || return 1)" + code=$? + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + COUNTIPS="$(sudo -n dash -c "$(declare -f Get_Fail2Ban_Status_Raw); Get_Fail2Ban_Status_Raw 2>&1" 2> /dev/null || return 2)" + code=$? + fi + + case $code in + 0) + if [[ $COUNTIPS -eq 0 ]]; then + # zero bans can be suspicious - something may not be working correctly + state="${aCOLOUR[YELLOW]}No IP(s) Banned${COLOUR_RESET}" + else + if [[ $COUNTIPS -gt 20 ]]; then + # high number of bans + state="${aCOLOUR[RED]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + else + # low number of bans + state="${aCOLOUR[GREY]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + fi + fi + ;; + 1) + state="${aCOLOUR[RED]}No Fail2Ban status available${COLOUR_RESET}" + ;; + 2) + state="${aCOLOUR[YELLOW]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" + ;; + *) + state="${aCOLOUR[RED]}Unknown state $COUNTIPS${COLOUR_RESET}" + ;; + esac + + echo "$state" + } + + Get_Network_Usage() + { + # IP address detection (IPv4) + local IP_re='([0-9]{1,3}\.){3}[0-9]{1,3}' + # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) + local err_re='timed out|timeout|could not resolve|not known|unknown' + + DATA_USAGE(){ + local netns="$1" + + # Use an array prefix so we can run commands either in a netns + # (ip netns exec ...) or the default namespace, and with sudo if needed + local -a ns_cmd_prefix=() sudo_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + sudo_cmd_prefix+=(sudo -n) + fi + + if [[ -n "$netns" ]]; then + ns_cmd_prefix+=(ip netns exec "$netns") + fi + + # Query interface list once inside the (optional) namespace and match names + mapfile -t observed_ifaces < <("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + + for IFACE in "${observed_ifaces[@]}"; do + # strip kernel-style peer suffix (e.g. eth0@if8) and use base name + local ifname="${IFACE%%@*}" + # skip loopback + [[ "$ifname" == lo ]] && continue + + case "$ifname" in + # wired / predictable names + eth*|en*|lan*) handle_iface=1 ;; + # wireless + wlan*|wl*) handle_iface=1 ;; + # vpn/tunnel/virtual + tun*|wg*|vpn*) handle_iface=1 ;; + *) continue ;; + esac + + if (( handle_iface )); then + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + + # Get RX data + if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi + + # Get TX data + if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi + + # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ + "$GREEN_BULLET" "${aCOLOUR[GREY]}" "$ip_addr ($ifname)" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$TX" "$RX" "$COLOUR_RESET" + fi + fi + done + } + + mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') + # Prepend an empty entry so the default (root) namespace can be handled in the loop + namespaces=( "" "${namespaces[@]}" ) + + for ns in "${namespaces[@]}"; do + raw_ns="${ns:-default}" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" + + if [[ "$(id -u)" = "0" ]]; then + # Running as root, can call function directly + IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + # Get public IP, sourcing globals inside the sudo'd shell + # Pass namespace as positional arg to avoid expansion issues inside the subshell + IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") + fi + + if [[ $IP =~ $IP_re ]]; then + # Valid IP detected, keep as-is and print with namespace + + # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + # IP="${BASH_REMATCH[0]}" + + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[BLUE]}" "$IP" "$display_ns" "$COLOUR_RESET" + + elif [[ -z $IP ]]; then + # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. + printf "%s %s %s %s%s%s\n" \ + "${aCOLOUR[RED]}" "Disconnected" "$GREEN_SEPARATOR" "${aCOLOUR[WHITE]}" "$display_ns" "$COLOUR_RESET" + + else + if [[ $IP =~ $err_re ]]; then + # Treat known curl/network error messages as timeout/DNS failures + IP="Timeout/DNS Failure" + fi + + # If the IP contains an unknown or error message, print it with the namespace + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[YELLOW]}" "$IP" "$display_ns" "$COLOUR_RESET" + fi + + DATA_USAGE "$ns" + done + } + + Print_Custom_Commands() + { + for i in "${!CUSTOM_COMMANDS[@]}"; do + cmd="${CUSTOM_COMMANDS[$i]}" + if [[ -n "$cmd" ]]; then + # shellcheck disable=SC2086 + echo $(eval "$cmd" 2>&1) || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + fi + done + } + + Get_Disk_Usage() + { + # If no arguments are provided, default to '"/" "/mnt/*"'. + if (( $# > 0 )); then + SPACE_BASE_PATTERNS=("$@") + fi + + # TODO: is there a more efficient reader than df (stat?) + # Use df's output to parse numeric fields (kB units) for mounts matching the supplied patterns + length=0 + results=() + while read -r size_kb used_kb avail_kb mnt_target; do + matched=0 + basename= + for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + # If the user supplies a pattern prefixed with 're:' treat it as regex + if [[ "$pattern" == re:* ]]; then + re="${pattern#re:}" + if [[ $mnt_target =~ $re ]]; then + matched=1 + basename=$(basename "$mnt_target") + break + fi + else + # Otherwise, treat it as a shell glob pattern and match using "case" + case "$mnt_target" in + $pattern) + matched=1 + basename=$(basename "$mnt_target") + break + ;; + *) ;; + esac + fi + done + if (( matched )); then + len=${#basename} + if (( len > length )); then + length=$len + fi + results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + fi + done < <(df -k --output=size,used,avail,target | tail -n +2) + + # Print results with aligned columns, converting kB to GB and calculating percentage used + for entry in "${results[@]}"; do + IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" + # name=$(basename "$entry") + # Do all math in kB, convert to GB for display and round to 1 decimal + if [[ "$size_kb" =~ ^[0-9]+$ ]]; then + size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + else + size_gb="N/A" + fi + if [[ "$used_kb" =~ ^[0-9]+$ ]]; then + used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + else + used_gb="N/A" + fi + if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") + else + avail_gb="N/A" + fi + if [[ "$size_kb" =~ ^[0-9]+$ && $size_kb -gt 0 ]]; then + perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + else + perc="N/A" + fi + # Print aligned columns: {basename} : {used} of {size} ({percent used}) + printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ + "$GREEN_BULLET" "${aCOLOUR[BLUE]}" "$name" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" + done + + } + + Get_VPN_Status() + { + local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" + case "$state" in + Connected*) state="${aCOLOUR[GREY]}$state${COLOUR_RESET}" ;; + Disconnected*) state="${aCOLOUR[RED]}$state${COLOUR_RESET}" ;; + *) state="${aCOLOUR[YELLOW]}$state${COLOUR_RESET}" ;; + esac + echo "$state" } Print_Banner_raw() @@ -277,55 +726,59 @@ $GREEN_LINE" # shellcheck disable=SC1091 (( ${aENABLED[14]} )) && . /boot/dietpi/func/dietpi-print_large "$(&1)" + (( ${aENABLED[1]} )) && Print_Item_State "${aDESCRIPTION[1]}" "$(uptime -p 2>&1)" # Linux kernel version - (( ${aENABLED[20]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[20]} $GREEN_SEPARATOR $(uname -r 2>&1)" + (( ${aENABLED[20]} )) && Print_Item_State "${aDESCRIPTION[20]}" "$(uname -r 2>&1)" # CPU temp - (( ${aENABLED[2]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[2]} $GREEN_SEPARATOR $(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" + (( ${aENABLED[2]} )) && Print_Item_State "${aDESCRIPTION[2]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" # RAM usage - (( ${aENABLED[17]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[17]} $GREEN_SEPARATOR $(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" + (( ${aENABLED[17]} )) && Print_Item_State "${aDESCRIPTION[17]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" # Load average - (( ${aENABLED[18]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[18]} $GREEN_SEPARATOR $(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" + (( ${aENABLED[18]} )) && Print_Item_State "${aDESCRIPTION[18]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" # Hostname - (( ${aENABLED[3]} && ! ${aENABLED[14]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[3]} $GREEN_SEPARATOR $(&1)" + (( ${aENABLED[4]} )) && Print_Item_State "${aDESCRIPTION[4]}" "$(hostname -y 2>&1)" # LAN IP - Print_Local_Ip + (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[6]} $GREEN_SEPARATOR $(G_GET_WAN_IP 2>&1)" + (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(G_GET_WAN_IP 2>&1)" # DietPi-VPN connection status - (( ${aENABLED[13]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[13]} $GREEN_SEPARATOR $(/boot/dietpi/dietpi-vpn status 2>&1)" + (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather - (( ${aENABLED[9]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[9]} $GREEN_SEPARATOR $(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" + (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status - if (( ${aENABLED[16]} )) - then - echo -en "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[16]} $GREEN_SEPARATOR " - if [[ $EUID == 0 ]] - then - # Running as root - Print_Cert_Status 2>&1 - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt, but print info instead - sudo -n dash -c "$(declare -f Print_Cert_Status); Print_Cert_Status 2>&1" 2> /dev/null || echo 'NOPASSWD sudo required to obtain cert status' - fi - fi - # Custom - (( ${aENABLED[10]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[10]} $GREEN_SEPARATOR $(bash "$FP_CUSTOM" 2>&1)" + (( ${aENABLED[16]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" + # Systemd status + (( ${aENABLED[23]} )) && Print_Item_State "${aDESCRIPTION[23]}" "$(Get_Systemd_Status 2>&1)" + # Fail2Ban status + (( ${aENABLED[24]} )) && Print_Item_State "${aDESCRIPTION[24]}" "$(Get_Fail2Ban_Status 2>&1)" + + # Network usage by namespace + # %b required so that percent symbols are not interpreted as format specifiers by printf + (( ${aENABLED[21]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" + # Disk Usage + (( ${aENABLED[22]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${SPACE_BASE_PATTERNS[@]}" 2>&1)" + + # Custom commands + (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + + # Separator line + (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$GREEN_LINE" + # MOTD if (( ${aENABLED[12]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" # shellcheck disable=SC1090 - [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[12]} $GREEN_SEPARATOR $motd" + [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" fi echo -e "$GREEN_LINE\n" @@ -370,17 +823,46 @@ $GREEN_LINE" if (( $i == 10 )) then - # Custom entry - [[ -f $FP_CUSTOM ]] && read -r G_WHIP_DEFAULT_ITEM < "$FP_CUSTOM" || G_WHIP_DEFAULT_ITEM='echo '\''Hello World!'\' - G_WHIP_INPUTBOX 'You have chosen to show a custom entry in the banner. -Please enter the desired command here.\n -NB: It is executed as bash script, so it needs to be in bash compatible syntax. - For multi-line or non-bash scripts, keep it separate and only add the script call here.' || continue - - echo "$G_WHIP_RETURNED_VALUE" > "$FP_CUSTOM" - - G_WHIP_DEFAULT_ITEM=${aDESCRIPTION[10]} - G_WHIP_INPUTBOX 'Please enter a meaningful name to be shown in front of your custom command output:' && aDESCRIPTION[10]=$G_WHIP_RETURNED_VALUE + example_command='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! $COLOUR_RESET :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + + # Manage custom commands list + while true; do + G_WHIP_MENU_ARRAY=( 'Add' 'Add a new command' 'Edit' 'Edit an existing command' 'Remove' 'Remove a command' 'Done' 'Finish custom commands configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU 'Manage custom banner commands.\n\nCAUTION!\nThese commands are executed with the same privileges as dietpi-banner :' || break + case "$G_WHIP_RETURNED_VALUE" in + Add) + G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue + CUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") + ;; + Edit) + if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + G_WHIP_MENU 'Select command to edit:' || continue + sel=$G_WHIP_RETURNED_VALUE + G_WHIP_DEFAULT_ITEM=${CUSTOM_COMMANDS[$sel]} + G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue + CUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" + ;; + Remove) + if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + G_WHIP_MENU 'Select command to remove:' || continue + sel=$G_WHIP_RETURNED_VALUE + unset 'CUSTOM_COMMANDS[$sel]' + # reindex array + CUSTOM_COMMANDS=("${CUSTOM_COMMANDS[@]}") + ;; + Done) + break + ;; + *) ;; + esac + done + + (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 elif (( $i == 19 )) then @@ -397,11 +879,53 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. G_WHIP_DEFAULT_ITEM=$BW_INDENT_FIXED G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE + + elif (( $i == 22 )) + then + pattern_hint="The matching mount 'target's returned by the 'df' command will be reported. +To use regex, start the pattern with 're:', otherwise shell globbing is used. +For example: '/mnt/*' or 're:^/mnt/.*/*$'" + + # Manage SPACE_BASE_PATTERNS list + while true; do + G_WHIP_MENU_ARRAY=( 'Add' 'Add a new pattern' 'Edit' 'Edit an existing pattern' 'Remove' 'Remove a pattern' 'Done' 'Finish pattern configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU "Manage disk space match patterns. $pattern_hint" || break + case "$G_WHIP_RETURNED_VALUE" in + Add) + G_WHIP_INPUTBOX "Enter new pattern :\n$pattern_hint" || continue + SPACE_BASE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") + ;; + Edit) + if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + G_WHIP_MENU 'Select pattern to edit :' || continue + sel=$G_WHIP_RETURNED_VALUE + G_WHIP_DEFAULT_ITEM=${SPACE_BASE_PATTERNS[$sel]} + G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue + SPACE_BASE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + ;; + Remove) + if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + G_WHIP_MENU 'Select pattern to remove :' || continue + sel=$G_WHIP_RETURNED_VALUE + unset 'SPACE_BASE_PATTERNS[$sel]' + # reindex array + SPACE_BASE_PATTERNS=("${SPACE_BASE_PATTERNS[@]}") + ;; + Done) + break + ;; + *) ;; + esac + done + (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 fi done - [[ -f $FP_CUSTOM ]] || aENABLED[10]=0 - Save > "$FP_SAVEFILE" } @@ -409,7 +933,7 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. # Main Loop #///////////////////////////////////////////////////////////////////////////////////// case $INPUT in - 0) Print_Header; Print_Local_Ip;; + 0) Print_Header; Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)";; 1) Print_Banner;; '') Menu_Main; Print_Banner;; *) G_DIETPI-NOTIFY 1 "Invalid input \"$*\"\n\nUsage:$USAGE"; exit 1;; @@ -418,4 +942,4 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. #----------------------------------------------------------------------------------- exit 0 #----------------------------------------------------------------------------------- -} +} \ No newline at end of file diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index b84cf27827..69474923d0 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -171,6 +171,27 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue } + # Truncate a string in the middle to a maximum length, inserting "..." + # - If the string is shorter than or equal to the maximum length, it is returned unchanged. + # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". + G_TRUNCATE_MID() { + local s="$1"; local max="$2" + local len=${#s} + if (( len <= max )); then + printf '%s' "$s" + return + fi + if (( max <= 3 )); then + printf '%.*s' "$max" "$s" + return + fi + local keep=$((max - 3)) + local pre=$(((keep + 1) / 2)) + local suf=$((keep / 2)) + local start=$((len - suf)) + printf '%s...%s' "${s:0:pre}" "${s:start}" + } + # DietPi-Notify # $1: # -2 = Processing @@ -1475,21 +1496,30 @@ Press any key to continue...' # Print public IP address and location info # - Optional arguments: # -t : Set timeout in seconds, supports floats, default: 3 + # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific + # network namespace, default: empty (no namespace = the main default namespace) G_GET_WAN_IP() { # Defaults local timeout=3 + local ns="" # Inputs while (( $# )) do # shellcheck disable=SC2015 case $1 in '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-n') shift; ns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done - curl -sSfLm "$timeout" 'https://dietpi.com/geoip' + + if [[ -n "$ns" ]]; then + echo $(ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + else + echo $(curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + fi } # $1 = directory to test permissions support From 6710a86046da090368b113d5ebc8afe6ffcef009 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:09:54 -0500 Subject: [PATCH 02/69] removed extra `declare`s and -g `global` --- dietpi/func/dietpi-banner | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 338e4ac69b..364b18b5f3 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -185,9 +185,9 @@ aCOLOUR[BLUE]=${aCOLOUR[6]} # Derived convenience strings - declare -g GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" - declare -g GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" - declare -g GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" + GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" + GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" + GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] From ae0add1617e7eae7f8a4c062c7f4fe48325a8160 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:17:36 -0500 Subject: [PATCH 03/69] adjusted example command for formatting and it serves as a test of colors inside custom commands --- dietpi/func/dietpi-banner | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 364b18b5f3..c5a2e337ed 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -168,7 +168,7 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + CUSTOM_COMMANDS[0]='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' # Default disk patterns SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") @@ -626,7 +626,7 @@ $GREEN_LINE" cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then # shellcheck disable=SC2086 - echo $(eval "$cmd" 2>&1) || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + echo "$(eval "$cmd" 2>&1)" || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" fi done } @@ -823,13 +823,13 @@ $GREEN_LINE" if (( $i == 10 )) then - example_command='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! $COLOUR_RESET :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + example_command='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' # Manage custom commands list while true; do G_WHIP_MENU_ARRAY=( 'Add' 'Add a new command' 'Edit' 'Edit an existing command' 'Remove' 'Remove a command' 'Done' 'Finish custom commands configuration' ) G_WHIP_DEFAULT_ITEM='Done' - G_WHIP_MENU 'Manage custom banner commands.\n\nCAUTION!\nThese commands are executed with the same privileges as dietpi-banner :' || break + G_WHIP_MENU 'Manage custom banner commands.\n\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit :' || break case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue @@ -862,6 +862,7 @@ $GREEN_LINE" esac done + # If no commands are configured, disable the custom commands option (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 elif (( $i == 19 )) From 68b18ddf4c9a485b14c54aef1cec21e6b865f6db Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 10:11:36 -0500 Subject: [PATCH 04/69] custom command failures provide the exit code --- dietpi/func/dietpi-banner | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c5a2e337ed..8c82b196c5 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -623,10 +623,16 @@ $GREEN_LINE" Print_Custom_Commands() { for i in "${!CUSTOM_COMMANDS[@]}"; do - cmd="${CUSTOM_COMMANDS[$i]}" + local cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then # shellcheck disable=SC2086 - echo "$(eval "$cmd" 2>&1)" || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + output="$(eval "$cmd" 2>&1)" + status=$? + if [[ $status -ne 0 ]]; then + printf '%b\n' "${aCOLOUR[RED]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + else + printf '%b\n' "$output" + fi fi done } From e59f0aeffbb782aebf59ed74e21a818dd7bfe101 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 13:58:47 -0500 Subject: [PATCH 05/69] Re-labeled colors for semantic indexing. removed extra formats. aCOLOUR saves just the relevant indexes. added some shellcheck bypasses and documentation. --- dietpi/func/dietpi-banner | 221 +++++++++++++++----------------------- 1 file changed, 85 insertions(+), 136 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 8c82b196c5..f7b0f76462 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -23,6 +23,8 @@ INPUT=$* # Import DietPi-Globals -------------------------------------------------------------- + # disable shellcheck for non-constant `source` (`.`) + # shellcheck disable=SC1090 . /boot/dietpi/func/dietpi-globals # - Allow concurrent banner prints but a single menu call only if [[ ! $INPUT ]] @@ -43,85 +45,31 @@ readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' # ---------------------------------------------------------------------------- - # Custom preferences initialisation helper (can be called by external scripts after - # sourcing this file). It loads the user-saved settings file + # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. - # Usage: DIETPI_BANNER_LOAD_PREFS + # Color and format references: + # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" + # `https://www.ditig.com/256-colors-cheat-sheet` + # `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` + # + # Usage: `DIETPI_BANNER_LOAD_PREFS` # ---------------------------------------------------------------------------- DIETPI_BANNER_LOAD_PREFS() { - declare -g COLOUR_RESET='\e[0m' declare -gA aCOLOUR=( - [0]='\e[38;5;154m' # DietPi green | Lines, bullets and separators - [1]='\e[1m' # Bold white | Main descriptions - [2]='\e[90m' # Grey | Subdued text - [3]='\e[91m' # Red | Update notifications / Bad state - [4]='\e[1;32m' # Green | Good state - [5]='\e[1;33m' # Yellow | Warning state - [6]='\e[1;36m' # Blue | Dynamic match - - [DIETPI_GREEN]='\e[38;5;154m' - [GREY]='\e[90m' - [RED]='\e[91m' - [GREEN]='\e[1;32m' - [YELLOW]='\e[1;33m' - [BLUE]='\e[1;36m' - [GOLD]='\e[38;5;220m' - [PURPLE]='\e[38;5;141m' - [VIVID_RED]='\e[38;5;196m' - [CYAN]='\e[38;5;45m' - - [RESET]='\e[0m' - [BOLD]='\e[1m' - [DIM]='\e[2m' - [UNDERLINE]='\e[4m' - [INVERSE]='\e[7m' - [BLINK]='\e[5m' - [HIDDEN]='\e[8m' - - [FG_BLACK]='\e[30m' - [FG_RED]='\e[31m' - [FG_GREEN]='\e[32m' - [FG_YELLOW]='\e[33m' - [FG_BLUE]='\e[34m' - [FG_MAGENTA]='\e[35m' - [FG_CYAN]='\e[36m' - [FG_WHITE]='\e[37m' - - [BRIGHT_BLACK]='\e[90m' - [BRIGHT_RED]='\e[91m' - [BRIGHT_GREEN]='\e[92m' - [BRIGHT_YELLOW]='\e[93m' - [BRIGHT_BLUE]='\e[94m' - [BRIGHT_MAGENTA]='\e[95m' - [BRIGHT_CYAN]='\e[96m' - [BRIGHT_WHITE]='\e[97m' - - [BG_BLACK]='\e[40m' - [BG_RED]='\e[41m' - [BG_GREEN]='\e[42m' - [BG_YELLOW]='\e[43m' - [BG_BLUE]='\e[44m' - [BG_MAGENTA]='\e[45m' - [BG_CYAN]='\e[46m' - [BG_WHITE]='\e[47m' - [BG_DARK_GREY]='\e[48;5;236m' - - [BG_BRIGHT_BLACK]='\e[100m' - [BG_BRIGHT_RED]='\e[101m' - [BG_BRIGHT_GREEN]='\e[102m' - [BG_BRIGHT_YELLOW]='\e[103m' - [BG_BRIGHT_BLUE]='\e[104m' - [BG_BRIGHT_MAGENTA]='\e[105m' - [BG_BRIGHT_CYAN]='\e[106m' - [BG_BRIGHT_WHITE]='\e[107m' - - [TRUE_GREEN]='\e[38;2;46;204;113m' - [TRUE_BG]='\e[48;2;44;62;80m' - + # Default colors + [ACCENT]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [STRONG]='\e[1m' # Bold white | Main descriptions + [WEAK]='\e[90m' # Grey | Subdued text + [ALERT]='\e[91m' # Red | Update notifications / Bad state + [GOOD]='\e[1;32m' # Green | Good state + [HIGHLIGHT]='\e[1;33m' # Yellow | Warning state + [ALT]='\e[1;36m' # Blue | Dynamic match ) + declare -g COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) - # Load settings here, to have chosen ${aCOLOUR[N]} applied to below strings + # Load settings here, to have chosen custom CLI colors applied + # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" } @@ -168,26 +116,17 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + CUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") # Load user preferences, overriding the above defaults as needed DIETPI_BANNER_LOAD_PREFS - # Put loaded colors into the used slots - aCOLOUR[DIETPI_GREEN]=${aCOLOUR[0]} - aCOLOUR[WHITE]=${aCOLOUR[1]} - aCOLOUR[GREY]=${aCOLOUR[2]} - aCOLOUR[RED]=${aCOLOUR[3]} - aCOLOUR[GREEN]=${aCOLOUR[4]} - aCOLOUR[YELLOW]=${aCOLOUR[5]} - aCOLOUR[BLUE]=${aCOLOUR[6]} - # Derived convenience strings - GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" - GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" - GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" + CLI_LINE=" ${aCOLOUR[ACCENT]}─────────────────────────────────────────────────────$COLOUR_RESET" + CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" + CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] @@ -255,8 +194,11 @@ # Persist only the primary colour slots (0..6). Convert any actual # ESC bytes to the two-character sequence \e so the saved file is # portable when sourced in different shells. - for i in {0..6} + for i in "${!aCOLOUR[@]}" do + # Don't save the RESET colour slot + [[ "$i" == "RESET" ]] && continue + val="${aCOLOUR[$i]}" # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') @@ -288,37 +230,37 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[YELLOW]}Update available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}Update available" # New DietPi live patches available? elif Check_DietPi_Live_Patches then - local text_update_available_date="${aCOLOUR[YELLOW]}New live patches available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}New live patches available" # APT update available? elif Check_APT_Updates then - local text_update_available_date="${aCOLOUR[YELLOW]}$PACKAGE_COUNT APT updates available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}$PACKAGE_COUNT APT updates available" # Reboot required to finalise kernel upgrade? elif Check_Reboot then - local text_update_available_date="${aCOLOUR[RED]}Reboot required" + local text_update_available_date="${aCOLOUR[ALERT]}Reboot required" else local locale=$(sed -n '/^[[:blank:]]*AUTO_SETUP_LOCALE=/{s/^[^=]*=//p;q}' /boot/dietpi.txt) local text_update_available_date=$(LC_ALL=${locale:-C.UTF-8} date '+%R - %a %x') fi - echo -e "$GREEN_LINE - ${aCOLOUR[WHITE]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET -$GREEN_LINE" + echo -e "$CLI_LINE + ${aCOLOUR[STRONG]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET +$CLI_LINE" } Print_Subheader() { # ON THE CURRENT LINE Output green line separator with subheader title "$1" - echo -e "\r$GREEN_LINE - ${aCOLOUR[WHITE]}$1 $GREEN_SEPARATOR" + echo -e "\r$CLI_LINE + ${aCOLOUR[STRONG]}$1 $CLI_SEPARATOR" } Get_Local_Ip() @@ -338,6 +280,7 @@ $GREEN_LINE" local i for i in /etc/letsencrypt/live/*/cert.pem do + # disable shellcheck for 'require-double-brackets' so it is `dash` compatible # shellcheck disable=SC2292 [ -f "$i" ] || continue openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' @@ -362,14 +305,14 @@ $GREEN_LINE" case $code in 0) case "$stat" in - Valid*) stat="${aCOLOUR[GREY]}$stat${COLOUR_RESET}" ;; - No\ certificate*) stat="${aCOLOUR[RED]}$stat${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[YELLOW]}$stat${COLOUR_RESET}" ;; + Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}" ;; + No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}" ;; esac ;; # stat is valid - 1) stat="${aCOLOUR[RED]}No certificate found${COLOUR_RESET}" ;; - 2) stat="${aCOLOUR[YELLOW]}NOPASSWD sudo required${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[YELLOW]}Unable to obtain cert status${COLOUR_RESET}" ;; + 1) stat="${aCOLOUR[ALERT]}No certificate found${COLOUR_RESET}" ;; + 2) stat="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[HIGHLIGHT]}Unable to obtain cert status${COLOUR_RESET}" ;; esac echo "$stat" @@ -377,7 +320,7 @@ $GREEN_LINE" Print_Credits() { - echo -e " ${aCOLOUR[GREY]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + echo -e " ${aCOLOUR[WEAK]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info @@ -392,32 +335,32 @@ $GREEN_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" # New DietPi live patches available? elif (( $LIVE_PATCHES )) then - echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" # APT updates available? elif (( $PACKAGE_COUNT )) then - echo -e " ${aCOLOUR[WHITE]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" # Reboot required to finalise kernel upgrade? elif (( $REBOOT_REQUIRED )) then - echo -e " ${aCOLOUR[WHITE]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[RED]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[ALERT]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" fi } Print_Useful_Commands() { - echo -e " ${aCOLOUR[WHITE]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[WHITE]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[WHITE]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation - ${aCOLOUR[WHITE]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor - ${aCOLOUR[WHITE]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[STRONG]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[STRONG]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation + ${aCOLOUR[STRONG]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor + ${aCOLOUR[STRONG]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" } Print_Item_State() @@ -427,9 +370,9 @@ $GREEN_LINE" local subtitle="$1" local state="$2" local width="${3:-}" - local color="${aCOLOUR[WHITE]}" + local color="${aCOLOUR[STRONG]}" printf "%b%b %-${width}b %b %b\n" \ - "$GREEN_BULLET" "${color}" "$subtitle" "$GREEN_SEPARATOR" "$state" + "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" } Get_Systemd_Status() @@ -437,9 +380,9 @@ $GREEN_LINE" # Print systemd overall status based on "systemctl --failed" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2>/dev/null || true) if [[ -z "$FAILED_CNT" || "$FAILED_CNT" -eq 0 ]]; then - state="${aCOLOUR[GREY]}No Services Failed${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" else - state="${aCOLOUR[RED]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" fi echo "$state" } @@ -480,25 +423,25 @@ $GREEN_LINE" 0) if [[ $COUNTIPS -eq 0 ]]; then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[YELLOW]}No IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned${COLOUR_RESET}" else if [[ $COUNTIPS -gt 20 ]]; then # high number of bans - state="${aCOLOUR[RED]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[GREY]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" fi fi ;; 1) - state="${aCOLOUR[RED]}No Fail2Ban status available${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}No Fail2Ban status available${COLOUR_RESET}" ;; 2) - state="${aCOLOUR[YELLOW]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" + state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" ;; *) - state="${aCOLOUR[RED]}Unknown state $COUNTIPS${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}Unknown state $COUNTIPS${COLOUR_RESET}" ;; esac @@ -567,7 +510,7 @@ $GREEN_LINE" if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$GREEN_BULLET" "${aCOLOUR[GREY]}" "$ip_addr ($ifname)" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$TX" "$RX" "$COLOUR_RESET" + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi fi done @@ -598,12 +541,12 @@ $GREEN_LINE" # IP="${BASH_REMATCH[0]}" printf " %s%s [%s]%s\n" \ - "${aCOLOUR[BLUE]}" "$IP" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" elif [[ -z $IP ]]; then # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. printf "%s %s %s %s%s%s\n" \ - "${aCOLOUR[RED]}" "Disconnected" "$GREEN_SEPARATOR" "${aCOLOUR[WHITE]}" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" else if [[ $IP =~ $err_re ]]; then @@ -613,7 +556,7 @@ $GREEN_LINE" # If the IP contains an unknown or error message, print it with the namespace printf " %s%s [%s]%s\n" \ - "${aCOLOUR[YELLOW]}" "$IP" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -625,11 +568,10 @@ $GREEN_LINE" for i in "${!CUSTOM_COMMANDS[@]}"; do local cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then - # shellcheck disable=SC2086 output="$(eval "$cmd" 2>&1)" status=$? if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[RED]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[ALERT]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -708,7 +650,7 @@ $GREEN_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ - "$GREEN_BULLET" "${aCOLOUR[BLUE]}" "$name" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" + "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" done } @@ -717,9 +659,9 @@ $GREEN_LINE" { local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" case "$state" in - Connected*) state="${aCOLOUR[GREY]}$state${COLOUR_RESET}" ;; - Disconnected*) state="${aCOLOUR[RED]}$state${COLOUR_RESET}" ;; - *) state="${aCOLOUR[YELLOW]}$state${COLOUR_RESET}" ;; + Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}" ;; + Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}" ;; + *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}" ;; esac echo "$state" } @@ -754,9 +696,9 @@ $GREEN_LINE" # DietPi-VPN connection status (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[7]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[7]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[8]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[8]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status @@ -776,17 +718,18 @@ $GREEN_LINE" (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands # Separator line - (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$GREEN_LINE" + (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$CLI_LINE" # MOTD if (( ${aENABLED[12]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" + # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" fi - echo -e "$GREEN_LINE\n" + echo -e "$CLI_LINE\n" (( ${aENABLED[15]} )) && Print_Credits Print_Updates @@ -829,7 +772,9 @@ $GREEN_LINE" if (( $i == 10 )) then - example_command='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + # ignore check for single/double quote expansion issue; unassigned var is referenced; + # shellcheck disable=SC2016,SC2154 + example_command='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' # Manage custom commands list while true; do @@ -889,6 +834,8 @@ $GREEN_LINE" elif (( $i == 22 )) then + # ignore check for single/double quote expansion issue; single/double quotes for glob; + # shellcheck disable=SC2016,SC2086 pattern_hint="The matching mount 'target's returned by the 'df' command will be reported. To use regex, start the pattern with 're:', otherwise shell globbing is used. For example: '/mnt/*' or 're:^/mnt/.*/*$'" @@ -929,6 +876,8 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" *) ;; esac done + + # If no patterns are configured, disable the disk usage option (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 fi done From 2a4b17dce237bcbcb606718bba0568792fa9111a Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 14:18:06 -0500 Subject: [PATCH 06/69] added colors reference --- dietpi/func/dietpi-banner | 1 + 1 file changed, 1 insertion(+) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index f7b0f76462..c800ab51ed 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -48,6 +48,7 @@ # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. # Color and format references: + # `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" # `https://www.ditig.com/256-colors-cheat-sheet` # `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` From be989f98aaa2253703a14d02d5ff08f643df22eb Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 15:57:19 -0500 Subject: [PATCH 07/69] combined sudo and ns command prefixes since they were only ever used together --- dietpi/func/dietpi-banner | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c800ab51ed..2123504a1b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -461,9 +461,9 @@ $CLI_LINE" # Use an array prefix so we can run commands either in a netns # (ip netns exec ...) or the default namespace, and with sudo if needed - local -a ns_cmd_prefix=() sudo_cmd_prefix=() + local -a ns_cmd_prefix=() if [[ "$(id -u)" -ne "0" ]]; then - sudo_cmd_prefix+=(sudo -n) + ns_cmd_prefix+=(sudo -n) fi if [[ -n "$netns" ]]; then @@ -471,7 +471,7 @@ $CLI_LINE" fi # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') for IFACE in "${observed_ifaces[@]}"; do # strip kernel-style peer suffix (e.g. eth0@if8) and use base name @@ -493,20 +493,20 @@ $CLI_LINE" local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr # Get RX data - if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" fi # Get TX data - if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" fi # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" ip_addr=" - No IP - " - ip_addr_cidr=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then From 9796927e63352d42414fe395ae4f35bf2e287c48 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:01:49 -0500 Subject: [PATCH 08/69] fixed syntax for shellcheck warning --- dietpi/func/dietpi-globals | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 69474923d0..214bd82be0 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -1506,9 +1506,8 @@ Press any key to continue...' # Inputs while (( $# )) do - # shellcheck disable=SC2015 case $1 in - '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; '-n') shift; ns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac From cd7654a268be96aaf93575b47e40ca98be731175 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:03:42 -0500 Subject: [PATCH 09/69] fixed indents --- dietpi/func/dietpi-banner | 160 +++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2123504a1b..4c4a17ae0e 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -457,64 +457,64 @@ $CLI_LINE" local err_re='timed out|timeout|could not resolve|not known|unknown' DATA_USAGE(){ - local netns="$1" + local netns="$1" - # Use an array prefix so we can run commands either in a netns - # (ip netns exec ...) or the default namespace, and with sudo if needed - local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi + # Use an array prefix so we can run commands either in a netns + # (ip netns exec ...) or the default namespace, and with sudo if needed + local -a ns_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + ns_cmd_prefix+=(sudo -n) + fi - if [[ -n "$netns" ]]; then - ns_cmd_prefix+=(ip netns exec "$netns") - fi + if [[ -n "$netns" ]]; then + ns_cmd_prefix+=(ip netns exec "$netns") + fi - # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') - - for IFACE in "${observed_ifaces[@]}"; do - # strip kernel-style peer suffix (e.g. eth0@if8) and use base name - local ifname="${IFACE%%@*}" - # skip loopback - [[ "$ifname" == lo ]] && continue - - case "$ifname" in - # wired / predictable names - eth*|en*|lan*) handle_iface=1 ;; - # wireless - wlan*|wl*) handle_iface=1 ;; - # vpn/tunnel/virtual - tun*|wg*|vpn*) handle_iface=1 ;; - *) continue ;; - esac + # Query interface list once inside the (optional) namespace and match names + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + + for IFACE in "${observed_ifaces[@]}"; do + # strip kernel-style peer suffix (e.g. eth0@if8) and use base name + local ifname="${IFACE%%@*}" + # skip loopback + [[ "$ifname" == lo ]] && continue + + case "$ifname" in + # wired / predictable names + eth*|en*|lan*) handle_iface=1 ;; + # wireless + wlan*|wl*) handle_iface=1 ;; + # vpn/tunnel/virtual + tun*|wg*|vpn*) handle_iface=1 ;; + *) continue ;; + esac - if (( handle_iface )); then - local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + if (( handle_iface )); then + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr - # Get RX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get RX data + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi - # Get TX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) - [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get TX data + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi - # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=" - No IP - " - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) - [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then - printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" - fi + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi - done + fi + done } mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') @@ -522,45 +522,45 @@ $CLI_LINE" namespaces=( "" "${namespaces[@]}" ) for ns in "${namespaces[@]}"; do - raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - - if [[ "$(id -u)" = "0" ]]; then - # Running as root, can call function directly - IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - # Get public IP, sourcing globals inside the sudo'd shell - # Pass namespace as positional arg to avoid expansion issues inside the subshell - IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") - fi + raw_ns="${ns:-default}" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - if [[ $IP =~ $IP_re ]]; then - # Valid IP detected, keep as-is and print with namespace + if [[ "$(id -u)" = "0" ]]; then + # Running as root, can call function directly + IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + # Get public IP, sourcing globals inside the sudo'd shell + # Pass namespace as positional arg to avoid expansion issues inside the subshell + IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") + fi - # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. - # IP="${BASH_REMATCH[0]}" + if [[ $IP =~ $IP_re ]]; then + # Valid IP detected, keep as-is and print with namespace - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + # IP="${BASH_REMATCH[0]}" - elif [[ -z $IP ]]; then - # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. - printf "%s %s %s %s%s%s\n" \ - "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" - else - if [[ $IP =~ $err_re ]]; then - # Treat known curl/network error messages as timeout/DNS failures - IP="Timeout/DNS Failure" - fi + elif [[ -z $IP ]]; then + # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. + printf "%s %s %s %s%s%s\n" \ + "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" - # If the IP contains an unknown or error message, print it with the namespace - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + else + if [[ $IP =~ $err_re ]]; then + # Treat known curl/network error messages as timeout/DNS failures + IP="Timeout/DNS Failure" fi - DATA_USAGE "$ns" + # If the IP contains an unknown or error message, print it with the namespace + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + fi + + DATA_USAGE "$ns" done } From 22d48d4d2fe0dee384f00789dbf9ea9cada23d26 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:24:12 -0500 Subject: [PATCH 10/69] added BANNER_GET_WAN_IP so dietpi-banner can have it's own behavior --- dietpi/func/dietpi-banner | 45 ++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 4c4a17ae0e..06e9751713 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -449,6 +449,39 @@ $CLI_LINE" echo "$state" } + # Print public IP address and location info + # - Optional arguments: + # -t : Set timeout in seconds, supports floats, default: 3 + # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific + # network namespace, default: empty (no namespace = the main default namespace) + BANNER_GET_WAN_IP() + { + # Defaults + local timeout=3 + local ns="" + # Inputs + while (( $# )) + do + case $1 in + '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-n') shift; ns="${1:-}";; + *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; + esac + shift + done + + local -a ns_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + ns_cmd_prefix+=(sudo -n) + fi + + if [[ -n "$ns" ]]; then + ns_cmd_prefix+=(ip netns exec "$ns") + fi + + "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 + } + Get_Network_Usage() { # IP address detection (IPv4) @@ -525,15 +558,7 @@ $CLI_LINE" raw_ns="${ns:-default}" display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - if [[ "$(id -u)" = "0" ]]; then - # Running as root, can call function directly - IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - # Get public IP, sourcing globals inside the sudo'd shell - # Pass namespace as positional arg to avoid expansion issues inside the subshell - IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") - fi + IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) if [[ $IP =~ $IP_re ]]; then # Valid IP detected, keep as-is and print with namespace @@ -693,7 +718,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(G_GET_WAN_IP 2>&1)" + (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(BANNER_GET_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) From cce4c40689dd1b3cc5789a25b6369d875b0926f0 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:27:45 -0500 Subject: [PATCH 11/69] fail2ban status now uses 'fail2ban-client banned'. removed unneeded read tests from network data usage. Get_Fail2Ban_Status is cleaner without the complicated sudo handling. --- dietpi/func/dietpi-banner | 107 ++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 06e9751713..0fcdbb0bfe 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -129,6 +129,10 @@ CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" + # IP address detection (IPv4) + IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' + IP4_re_cidr='([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}' + DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] then @@ -192,7 +196,7 @@ echo "aENABLED[$i]=${aENABLED[$i]}" done - # Persist only the primary colour slots (0..6). Convert any actual + # Persist only the primary colour slots. Convert any actual # ESC bytes to the two-character sequence \e so the saved file is # portable when sourced in different shells. for i in "${!aCOLOUR[@]}" @@ -392,57 +396,56 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - # # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. - # # also is `dash` compatible - - # Count unique banned IPs via iptables/nftables compatibility. Try iptables first, - # fall back to nft if available. - COUNTIPS=0 - if command -v iptables >/dev/null 2>&1; then - COUNTIPS=$(iptables -L -n 2>/dev/null | awk '/REJECT/ {print $0}' | uniq -c | wc -l) - elif command -v nft >/dev/null 2>&1; then - COUNTIPS=$(nft list ruleset 2>/dev/null | awk '/reject/ {print $0}' | uniq -c | wc -l) + if command -v fail2ban-client >/dev/null 2>&1; then + if [[ "$(id -u)" = "0" ]]; then + f2b_output="$(fail2ban-client banned 2>/dev/null)" + else + f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" + [ $? = 2 ] && return 2 + fi + + if ! command -v sort >/dev/null 2>&1; then + sort() { cat; } # fallback if sort is not available, simply passes input through + fi + + # get banned IP address count + IP_addrs="$(echo "$f2b_output" | grep -o '\[[^]]*\]' | grep -Eo "$IP4_re" | sort -u)" + count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" + echo "$count_banned_ips" + + return 0 else + # fail2ban-client command is not available return 1 fi - echo "$COUNTIPS" - return 0 } - COUNTIPS=0 - if [[ "$(id -u)" = "0" ]] - then - COUNTIPS="$(Get_Fail2Ban_Status_Raw 2>/dev/null || return 1)" - code=$? - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - COUNTIPS="$(sudo -n dash -c "$(declare -f Get_Fail2Ban_Status_Raw); Get_Fail2Ban_Status_Raw 2>&1" 2> /dev/null || return 2)" - code=$? - fi + count_banned_ips=$(Get_Fail2Ban_Status_Raw) + code=$? case $code in 0) - if [[ $COUNTIPS -eq 0 ]]; then + if [[ $count_banned_ips -eq 0 ]]; then # zero bans can be suspicious - something may not be working correctly state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned${COLOUR_RESET}" else - if [[ $COUNTIPS -gt 20 ]]; then + if [[ $count_banned_ips -gt 20 ]]; then # high number of bans - state="${aCOLOUR[ALERT]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[WEAK]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi ;; 1) - state="${aCOLOUR[ALERT]}No Fail2Ban status available${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}fail2ban-client not available${COLOUR_RESET}" ;; 2) state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" ;; *) - state="${aCOLOUR[ALERT]}Unknown state $COUNTIPS${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}" ;; esac @@ -484,8 +487,6 @@ $CLI_LINE" Get_Network_Usage() { - # IP address detection (IPv4) - local IP_re='([0-9]{1,3}\.){3}[0-9]{1,3}' # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) local err_re='timed out|timeout|could not resolve|not known|unknown' @@ -514,38 +515,32 @@ $CLI_LINE" case "$ifname" in # wired / predictable names - eth*|en*|lan*) handle_iface=1 ;; + eth*|en*|lan*) : ;; # wireless - wlan*|wl*) handle_iface=1 ;; + wlan*|wl*) : ;; # vpn/tunnel/virtual - tun*|wg*|vpn*) handle_iface=1 ;; + tun*|wg*|vpn*) : ;; *) continue ;; esac - if (( handle_iface )); then - local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr - # Get RX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get RX data + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # Get TX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) - [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get TX data + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=" - No IP - " - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) - [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then - printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" - fi + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %11s RX= %11s%s\n" \ + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi done } @@ -556,11 +551,11 @@ $CLI_LINE" for ns in "${namespaces[@]}"; do raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 24)" IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) - if [[ $IP =~ $IP_re ]]; then + if [[ $IP =~ $IP4_re ]]; then # Valid IP detected, keep as-is and print with namespace # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. From 2de09335bed824ad3f6093629f7a7894a866bc93 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:57:35 -0500 Subject: [PATCH 12/69] on network usage, only do sudo if netns is requested --- dietpi/func/dietpi-banner | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0fcdbb0bfe..a56cf29059 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -461,25 +461,26 @@ $CLI_LINE" { # Defaults local timeout=3 - local ns="" + local netns="" # Inputs while (( $# )) do case $1 in '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; - '-n') shift; ns="${1:-}";; + '-n') shift; netns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi - - if [[ -n "$ns" ]]; then - ns_cmd_prefix+=(ip netns exec "$ns") + if [[ -n "$netns" ]]; then + if [[ "$(id -u)" -ne "0" ]]; then + # add sudo if not root + ns_cmd_prefix+=(sudo -n) + fi + # add netns prefix + ns_cmd_prefix+=(ip netns exec "$netns") fi "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 @@ -496,11 +497,12 @@ $CLI_LINE" # Use an array prefix so we can run commands either in a netns # (ip netns exec ...) or the default namespace, and with sudo if needed local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi - if [[ -n "$netns" ]]; then + if [[ "$(id -u)" -ne "0" ]]; then + # add sudo if not root + ns_cmd_prefix+=(sudo -n) + fi + # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi From 246cfadc210950d7d6f7df99ee614ff54cbfcdd0 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 12:26:26 -0500 Subject: [PATCH 13/69] Add example G_TRUNCATE_MID --- CONTRIBUTING.md | 10 ++++++++++ dietpi/func/dietpi-globals | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34da9a7fd9..29c28614bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -199,6 +199,16 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. log=1 G_WHIP_VIEWFILE "$FP_LOG" || return ``` +- `G_TRUNCATE_MID` (shorten long strings by squishing the middle characters): + ``` + G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26 + # -> "Long text to... the middle" + G_TRUNCATE_MID "Long text" 5 + # -> "L...t" + G_TRUNCATE_MID "Text" 3 + # -> "Tex" + ``` + - `Save()` persistence pattern (follow dietpi-banner conventions): ``` Save(){ diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 214bd82be0..6043a74b3e 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -174,6 +174,11 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # Truncate a string in the middle to a maximum length, inserting "..." # - If the string is shorter than or equal to the maximum length, it is returned unchanged. # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". + # - E.g. + # For example: + # `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" + # `G_TRUNCATE_MID "Long text" 5` returns "L...t" + # `G_TRUNCATE_MID "Text" 3` returns "Tex" G_TRUNCATE_MID() { local s="$1"; local max="$2" local len=${#s} From 0eacc2172d595ae35ea7d1969e717b1db632990d Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:04:29 -0500 Subject: [PATCH 14/69] improved disk space handling with fallbacks for missing data --- dietpi/func/dietpi-banner | 47 +++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a56cf29059..71e2a5d087 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -627,6 +627,7 @@ $CLI_LINE" fi else # Otherwise, treat it as a shell glob pattern and match using "case" + # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement case "$mnt_target" in $pattern) matched=1 @@ -649,28 +650,56 @@ $CLI_LINE" # Print results with aligned columns, converting kB to GB and calculating percentage used for entry in "${results[@]}"; do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" - # name=$(basename "$entry") + + local values_count=0 # Do all math in kB, convert to GB for display and round to 1 decimal if [[ "$size_kb" =~ ^[0-9]+$ ]]; then - size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + values_count=$((values_count+1)) + size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") else - size_gb="N/A" + if [[ "$used_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + # If used and available are available, calculate size as used + available + size_kb=$((used_kb+avail_kb)) + size_gb=$(awk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") + else + size_gb="???" + fi fi + if [[ "$used_kb" =~ ^[0-9]+$ ]]; then - used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + values_count=$((values_count+1)) + used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") else - used_gb="N/A" + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + # If size and available are available, calculate used as size - available + used_kb=$((size_kb-avail_kb)) + used_gb=$(awk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") + else + used_gb="???" + fi fi + if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + values_count=$((values_count+1)) avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") else - avail_gb="N/A" + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$used_kb" =~ ^[0-9]+$ ]]; then + # If size and used are available, calculate available as size - used + avail_kb=$((size_kb-used_kb)) + avail_gb=$(awk "BEGIN {printf \"%.1f \", ($avail_kb)/1024/1024}") + else + avail_gb="???" + fi fi - if [[ "$size_kb" =~ ^[0-9]+$ && $size_kb -gt 0 ]]; then - perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + + # Calculate percentage used based on available data: + if [[ "$values_count" -ge 2 ]]; then + # this should match the `df` output + perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") else - perc="N/A" + perc="?? %" # default unknown value fi + # Print aligned columns: {basename} : {used} of {size} ({percent used}) printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" From 0a5d05b4948d994313e1d7f56543ad2f1f3ea926 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:06:31 -0500 Subject: [PATCH 15/69] fix indents --- dietpi/func/dietpi-banner | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 71e2a5d087..7a35fae559 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -203,7 +203,7 @@ do # Don't save the RESET colour slot [[ "$i" == "RESET" ]] && continue - + val="${aCOLOUR[$i]}" # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') @@ -614,37 +614,37 @@ $CLI_LINE" length=0 results=() while read -r size_kb used_kb avail_kb mnt_target; do - matched=0 - basename= - for pattern in "${SPACE_BASE_PATTERNS[@]}"; do - # If the user supplies a pattern prefixed with 're:' treat it as regex - if [[ "$pattern" == re:* ]]; then - re="${pattern#re:}" - if [[ $mnt_target =~ $re ]]; then - matched=1 - basename=$(basename "$mnt_target") - break - fi - else - # Otherwise, treat it as a shell glob pattern and match using "case" - # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement - case "$mnt_target" in - $pattern) - matched=1 - basename=$(basename "$mnt_target") - break - ;; - *) ;; - esac - fi - done - if (( matched )); then - len=${#basename} - if (( len > length )); then - length=$len - fi - results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + matched=0 + basename= + for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + # If the user supplies a pattern prefixed with 're:' treat it as regex + if [[ "$pattern" == re:* ]]; then + re="${pattern#re:}" + if [[ $mnt_target =~ $re ]]; then + matched=1 + basename=$(basename "$mnt_target") + break + fi + else + # Otherwise, treat it as a shell glob pattern and match using "case" + # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement + case "$mnt_target" in + $pattern) + matched=1 + basename=$(basename "$mnt_target") + break + ;; + *) ;; + esac fi + done + if (( matched )); then + len=${#basename} + if (( len > length )); then + length=$len + fi + results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + fi done < <(df -k --output=size,used,avail,target | tail -n +2) # Print results with aligned columns, converting kB to GB and calculating percentage used From 510858d17cbfe98644904f8f3b3bc874f6f714e9 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 15:58:25 -0500 Subject: [PATCH 16/69] refactored for associative indexing. includes a migration feature --- dietpi/func/dietpi-banner | 295 ++++++++++++++++++++++++-------------- 1 file changed, 189 insertions(+), 106 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 7a35fae559..2ebe89a982 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -73,43 +73,108 @@ # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + + # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form + # Only perform migration when numeric indices exist in the loaded aENABLED + if declare -p aENABLED >/dev/null 2>&1; then + local has_numeric=0 + for k in "${!aENABLED[@]}"; do + if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric=1; break; fi + done + if (( has_numeric )); then + # Prefer LEGACY_INDEXED_BANNER_KEYS for a stable mapping; fallback to MENU_ITEMS + local -n key_map_ref=LEGACY_INDEXED_BANNER_KEYS + if [[ ${#LEGACY_INDEXED_BANNER_KEYS[@]} -eq 0 && ${#MENU_ITEMS[@]} -gt 0 ]]; then + key_map_ref=MENU_ITEMS + fi + declare -gA _tmp_enabled=() + # First, map numeric indices explicitly so legacy numeric prefs take precedence + for (( i=0; i<${#key_map_ref[@]}; i++ )); do + if [[ -n "${aENABLED[$i]+x}" ]]; then + key=${key_map_ref[$i]} + [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$i]:-0} + fi + done + # Then copy any remaining non-numeric keys that weren't set by numeric mapping + for idx in "${!aENABLED[@]}"; do + if [[ ! "$idx" =~ ^[0-9]+$ ]]; then + key=$idx + # don't overwrite keys already set by numeric mapping + [[ -n "${_tmp_enabled[$key]+x}" ]] && continue + [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$idx]:-0} + fi + done + # replace aENABLED with associative mapping + unset aENABLED + declare -gA aENABLED + for k in "${!_tmp_enabled[@]}"; do aENABLED[$k]=${_tmp_enabled[$k]}; done + + # Mark that a migration from legacy numeric indices occurred during this run + declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=1 + unset _tmp_enabled + fi + fi } - aDESCRIPTION=( - - 'Device model' - 'Uptime' - 'CPU temp' - 'FQDN/hostname' - 'NIS domainname' - 'LAN IP' - 'WAN IP' - 'Disk usage (RootFS)' - 'Disk usage (userdata)' - 'Weather (wttr.in)' - 'Custom commands' - 'Display DietPi useful commands?' - 'MOTD' - 'VPN status' - 'Large hostname' - 'Print credits' - 'Let'\''s Encrypt cert status' - 'RAM usage' - 'Load average' - 'Word-wrap lines on small screens' - 'Kernel' - 'Network Usage' - 'Disk Usage' - 'Systemd Status' - 'Fail2Ban Status' + # Use associative arrays keyed by short names derived from the description + declare -gA aDESCRIPTION=( + [device_model]='Device model' + [uptime]='Uptime' + [cpu_temp]='CPU temp' + [hostname]='FQDN/hostname' + [nis_domainname]='NIS domainname' + [lan_ip]='LAN IP' + [wan_ip]='WAN IP' + [disk_rootfs]='Disk usage (RootFS)' + [disk_userdata]='Disk usage (userdata)' + [weather]='Weather (wttr.in)' + [custom_commands]='Custom commands' + [useful_commands]='Display DietPi useful commands?' + [motd]='MOTD' + [vpn_status]='VPN status' + [large_hostname]='Large hostname' + [print_credits]='Print credits' + [letsencrypt]='Let'\''s Encrypt cert status' + [ram_usage]='RAM usage' + [load_average]='Load average' + [word_wrap]='Word-wrap lines on small screens' + [kernel]='Kernel' + [network_usage]='Network Usage' + [disk_usage]='Disk Usage' + [systemd_status]='Systemd Status' + [fail2ban_status]='Fail2Ban Status' + ) + # Set the array order for the menu, items must be listed here to show up + declare -ga MENU_ITEMS=( + large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname \ + lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status \ + network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap kernel + ) + + # Keep a fixed index->key mapping to support numeric-index migration even if MENU_ITEMS changes later + # LEGACY_INDEXED_BANNER_KEYS maps legacy numeric indices to the current key names (0 -> device_model, 1 -> uptime, ...) + ############## + ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ############## + declare -ga LEGACY_INDEXED_BANNER_KEYS=( + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status ) + ############## + ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ############## + # Set defaults: Disable CPU temp by default in VMs + declare -gA aENABLED if (( $G_HW_MODEL == 20 )) then - aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) + aENABLED=( + [device_model]=1 [uptime]=0 [cpu_temp]=0 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=1 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 + ) else - aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) + aENABLED=( + [device_model]=1 [uptime]=0 [cpu_temp]=1 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=0 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 + ) fi # Folding type (colon, dash, fixed), and fixed indent @@ -117,11 +182,13 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' + aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns - SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") + aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") # Load user preferences, overriding the above defaults as needed + # Flag indicating whether a legacy numeric->associative migration happened during this run + declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 DIETPI_BANNER_LOAD_PREFS # Derived convenience strings @@ -191,9 +258,10 @@ Save() { - for i in "${!aDESCRIPTION[@]}" - do - echo "aENABLED[$i]=${aENABLED[$i]}" + # Persist associative aENABLED as simple assignments (one per line) + for key in "${MENU_ITEMS[@]}"; do + val=${aENABLED[$key]:-0} + printf "aENABLED[%s]=%s\n" "$key" "$val" done # Persist only the primary colour slots. Convert any actual @@ -212,22 +280,27 @@ echo "aCOLOUR[$i]='$esc'" done - # Persist SPACE_BASE_PATTERNS (indices 0..N) - for i in "${!SPACE_BASE_PATTERNS[@]}"; do - patt=${SPACE_BASE_PATTERNS[$i]} + # Persist aDISK_SPACE_PATTERNS (indices 0..N) + for i in "${!aDISK_SPACE_PATTERNS[@]}"; do + patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} - printf "SPACE_BASE_PATTERNS[%s]='%s'\n" "$i" "$patt" + printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" done - # Persist CUSTOM_COMMANDS (indices 0..N) - for i in "${!CUSTOM_COMMANDS[@]}"; do - cmd=${CUSTOM_COMMANDS[$i]} + # Persist aCUSTOM_COMMANDS (indices 0..N) + for i in "${!aCUSTOM_COMMANDS[@]}"; do + cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} - printf "CUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" + printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" done + # Persist BW indent settings echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" echo "BW_INDENT_FIXED=$BW_INDENT_FIXED" + + # Clear migration flag now that prefs are persisted + DIETPI_BANNER_MIGRATED_DURING_RUN=0 + return 0 } Print_Header() @@ -270,7 +343,7 @@ $CLI_LINE" Get_Local_Ip() { - (( ${aENABLED[5]} )) || return 0 + (( ${aENABLED[lan_ip]} )) || return 0 local iface=$(G_GET_NET -q iface) local ip=$(G_GET_NET -q ip) echo "${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" @@ -588,8 +661,8 @@ $CLI_LINE" Print_Custom_Commands() { - for i in "${!CUSTOM_COMMANDS[@]}"; do - local cmd="${CUSTOM_COMMANDS[$i]}" + for i in "${!aCUSTOM_COMMANDS[@]}"; do + local cmd="${aCUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then output="$(eval "$cmd" 2>&1)" status=$? @@ -606,7 +679,7 @@ $CLI_LINE" { # If no arguments are provided, default to '"/" "/mnt/*"'. if (( $# > 0 )); then - SPACE_BASE_PATTERNS=("$@") + aDISK_SPACE_PATTERNS=("$@") fi # TODO: is there a more efficient reader than df (stat?) @@ -616,7 +689,7 @@ $CLI_LINE" while read -r size_kb used_kb avail_kb mnt_target; do matched=0 basename= - for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + for pattern in "${aDISK_SPACE_PATTERNS[@]}"; do # If the user supplies a pattern prefixed with 're:' treat it as regex if [[ "$pattern" == re:* ]]; then re="${pattern#re:}" @@ -724,68 +797,71 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[14]} )) && . /boot/dietpi/func/dietpi-print_large "$(&1)" + (( ${aENABLED[uptime]} )) && Print_Item_State "${aDESCRIPTION[uptime]}" "$(uptime -p 2>&1)" # Linux kernel version - (( ${aENABLED[20]} )) && Print_Item_State "${aDESCRIPTION[20]}" "$(uname -r 2>&1)" + (( ${aENABLED[kernel]} )) && Print_Item_State "${aDESCRIPTION[kernel]}" "$(uname -r 2>&1)" # CPU temp - (( ${aENABLED[2]} )) && Print_Item_State "${aDESCRIPTION[2]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" + (( ${aENABLED[cpu_temp]} )) && Print_Item_State "${aDESCRIPTION[cpu_temp]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" # RAM usage - (( ${aENABLED[17]} )) && Print_Item_State "${aDESCRIPTION[17]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" + (( ${aENABLED[ram_usage]} )) && Print_Item_State "${aDESCRIPTION[ram_usage]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" # Load average - (( ${aENABLED[18]} )) && Print_Item_State "${aDESCRIPTION[18]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" + (( ${aENABLED[load_average]} )) && Print_Item_State "${aDESCRIPTION[load_average]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" # Hostname - (( ${aENABLED[3]} && ! ${aENABLED[14]} )) && Print_Item_State "${aDESCRIPTION[3]}" "$(&1)" + (( ${aENABLED[nis_domainname]} )) && Print_Item_State "${aDESCRIPTION[nis_domainname]}" "$(hostname -y 2>&1)" # LAN IP - (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" + (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(BANNER_GET_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(BANNER_GET_WAN_IP 2>&1)" # DietPi-VPN connection status - (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" + (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[7]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_rootfs]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_rootfs]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[8]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_userdata]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_userdata]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather - (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" + (( ${aENABLED[weather]} )) && Print_Item_State "${aDESCRIPTION[weather]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status - (( ${aENABLED[16]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" + (( ${aENABLED[letsencrypt]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" # Systemd status - (( ${aENABLED[23]} )) && Print_Item_State "${aDESCRIPTION[23]}" "$(Get_Systemd_Status 2>&1)" + (( ${aENABLED[systemd_status]} )) && Print_Item_State "${aDESCRIPTION[systemd_status]}" "$(Get_Systemd_Status 2>&1)" # Fail2Ban status - (( ${aENABLED[24]} )) && Print_Item_State "${aDESCRIPTION[24]}" "$(Get_Fail2Ban_Status 2>&1)" + (( ${aENABLED[fail2ban_status]} )) && Print_Item_State "${aDESCRIPTION[fail2ban_status]}" "$(Get_Fail2Ban_Status 2>&1)" # Network usage by namespace # %b required so that percent symbols are not interpreted as format specifiers by printf - (( ${aENABLED[21]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" + (( ${aENABLED[network_usage]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" # Disk Usage - (( ${aENABLED[22]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${SPACE_BASE_PATTERNS[@]}" 2>&1)" + (( ${aENABLED[disk_usage]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1)" # Custom commands - (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + (( ${aENABLED[custom_commands]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands # Separator line - (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$CLI_LINE" + (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" # MOTD - if (( ${aENABLED[12]} )) + if (( ${aENABLED[motd]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 - [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" + [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[motd]}" "$motd" fi echo -e "$CLI_LINE\n" - (( ${aENABLED[15]} )) && Print_Credits + (( ${aENABLED[print_credits]} )) && Print_Credits Print_Updates - (( ${aENABLED[11]} )) && Print_Useful_Commands + (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands + + # Notify user if prefs were migrated from legacy numeric indices during this run + (( DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } Print_Banner() @@ -793,7 +869,7 @@ $CLI_LINE" G_TERM_CLEAR # Pipe to awk script for word-wrap if enabled - if (( ${aENABLED[19]} )) + if (( ${aENABLED[word_wrap]} )) then Print_Banner_raw | mawk -v "MAXCOL=$(tput cols)" -v "INDENT_TYPE=$BW_INDENT_TYPE" -v "INDENT_FIXED=$BW_INDENT_FIXED" -f "$FP_BANNERWRAP_AWK" else @@ -804,25 +880,25 @@ $CLI_LINE" Menu_Main() { G_WHIP_CHECKLIST_ARRAY=() - for i in "${!aDESCRIPTION[@]}" + for key in "${MENU_ITEMS[@]}" do local state='off' - (( ${aENABLED[$i]:=0} == 1 )) && state='on' - G_WHIP_CHECKLIST_ARRAY+=("$i" "${aDESCRIPTION[$i]}" "$state") + (( ${aENABLED[$key]:=0} == 1 )) && state='on' + G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") done G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 - for i in "${!aDESCRIPTION[@]}" + for key in "${MENU_ITEMS[@]}" do - aENABLED[$i]=0 + aENABLED[$key]=0 done - for i in $G_WHIP_RETURNED_VALUE + for selection in $G_WHIP_RETURNED_VALUE do - aENABLED[$i]=1 + aENABLED[$selection]=1 - if (( $i == 10 )) + if [[ "$selection" == "custom_commands" ]] then # ignore check for single/double quote expansion issue; unassigned var is referenced; # shellcheck disable=SC2016,SC2154 @@ -836,27 +912,27 @@ $CLI_LINE" case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue - CUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") + aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) - if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi + if (( ${#aCUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done G_WHIP_MENU 'Select command to edit:' || continue sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${CUSTOM_COMMANDS[$sel]} + G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue - CUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" + aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) - if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi + if (( ${#aCUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done G_WHIP_MENU 'Select command to remove:' || continue sel=$G_WHIP_RETURNED_VALUE - unset 'CUSTOM_COMMANDS[$sel]' + unset 'aCUSTOM_COMMANDS[$sel]' # reindex array - CUSTOM_COMMANDS=("${CUSTOM_COMMANDS[@]}") + aCUSTOM_COMMANDS=("${aCUSTOM_COMMANDS[@]}") ;; Done) break @@ -866,9 +942,9 @@ $CLI_LINE" done # If no commands are configured, disable the custom commands option - (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 + (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && aENABLED[custom_commands]=0 - elif (( $i == 19 )) + elif [[ "$selection" == "word_wrap" ]] then # Banner word-wrap options G_WHIP_MENU_ARRAY=( @@ -884,7 +960,7 @@ $CLI_LINE" G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE - elif (( $i == 22 )) + elif [[ "$selection" == "disk_usage" ]] then # ignore check for single/double quote expansion issue; single/double quotes for glob; # shellcheck disable=SC2016,SC2086 @@ -892,7 +968,7 @@ $CLI_LINE" To use regex, start the pattern with 're:', otherwise shell globbing is used. For example: '/mnt/*' or 're:^/mnt/.*/*$'" - # Manage SPACE_BASE_PATTERNS list + # Manage aDISK_SPACE_PATTERNS list while true; do G_WHIP_MENU_ARRAY=( 'Add' 'Add a new pattern' 'Edit' 'Edit an existing pattern' 'Remove' 'Remove a pattern' 'Done' 'Finish pattern configuration' ) G_WHIP_DEFAULT_ITEM='Done' @@ -900,27 +976,27 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new pattern :\n$pattern_hint" || continue - SPACE_BASE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") + aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) - if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi + if (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done G_WHIP_MENU 'Select pattern to edit :' || continue sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${SPACE_BASE_PATTERNS[$sel]} + G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue - SPACE_BASE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) - if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi + if (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done G_WHIP_MENU 'Select pattern to remove :' || continue sel=$G_WHIP_RETURNED_VALUE - unset 'SPACE_BASE_PATTERNS[$sel]' + unset 'aDISK_SPACE_PATTERNS[$sel]' # reindex array - SPACE_BASE_PATTERNS=("${SPACE_BASE_PATTERNS[@]}") + aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") ;; Done) break @@ -930,10 +1006,17 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" done # If no patterns are configured, disable the disk usage option - (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 + (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && aENABLED[disk_usage]=0 fi done + # Before saving, create a backup of the existing prefs if migrating. + # FP_SAVEFILE gets truncated when calling Save() below, so we need to backup first. + if [[ -f "$FP_SAVEFILE" ]] && [[ $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]]; then + backup="$FP_SAVEFILE.bak-$(date -Iseconds)" + cp -a "$FP_SAVEFILE" "$backup" || true + fi + Save > "$FP_SAVEFILE" } @@ -941,7 +1024,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Main Loop #///////////////////////////////////////////////////////////////////////////////////// case $INPUT in - 0) Print_Header; Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)";; + 0) Print_Header; Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)";; 1) Print_Banner;; '') Menu_Main; Print_Banner;; *) G_DIETPI-NOTIFY 1 "Invalid input \"$*\"\n\nUsage:$USAGE"; exit 1;; From ad499c2308eca52eb4af83b2f33407f0e7316214 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 16:46:07 -0500 Subject: [PATCH 17/69] fixed shellchecks --- dietpi/func/dietpi-banner | 8 ++++---- dietpi/func/dietpi-globals | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2ebe89a982..c0856eeea7 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -47,7 +47,7 @@ # ---------------------------------------------------------------------------- # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. - # Color and format references: + # Color and format references: # `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" # `https://www.ditig.com/256-colors-cheat-sheet` @@ -163,7 +163,6 @@ ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION ############## - # Set defaults: Disable CPU temp by default in VMs declare -gA aENABLED if (( $G_HW_MODEL == 20 )) @@ -182,6 +181,7 @@ BW_INDENT_FIXED=3 # Default custom command example + # shellcheck disable=SC2016 # Disable warning about single quotes in echo -e aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") @@ -474,7 +474,7 @@ $CLI_LINE" f2b_output="$(fail2ban-client banned 2>/dev/null)" else f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" - [ $? = 2 ] && return 2 + [[ $? = 2 ]] && return 2 fi if ! command -v sort >/dev/null 2>&1; then @@ -859,7 +859,7 @@ $CLI_LINE" (( ${aENABLED[print_credits]} )) && Print_Credits Print_Updates (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands - + # Notify user if prefs were migrated from legacy numeric indices during this run (( DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 6043a74b3e..8bc94d39fd 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -174,7 +174,7 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # Truncate a string in the middle to a maximum length, inserting "..." # - If the string is shorter than or equal to the maximum length, it is returned unchanged. # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". - # - E.g. + # - E.g. # For example: # `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" # `G_TRUNCATE_MID "Long text" 5` returns "L...t" @@ -1520,9 +1520,9 @@ Press any key to continue...' done if [[ -n "$ns" ]]; then - echo $(ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 else - echo $(curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 fi } From 905cb3ffdc14712fc596bb5877100b173f58e39b Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 17:48:18 -0500 Subject: [PATCH 18/69] to accomodate associative indexing, added enumeration option to G_WHIP_CHECKLIST --- dietpi/func/dietpi-banner | 2 ++ dietpi/func/dietpi-globals | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c0856eeea7..0a96b369c2 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -887,7 +887,9 @@ $CLI_LINE" G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") done + G_WHIP_CHECKLIST_ENUM=1 # set the menu to use integer indices for the display G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 + G_WHIP_CHECKLIST_ENUM=0 for key in "${MENU_ITEMS[@]}" do diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 8bc94d39fd..f3a184092d 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -486,6 +486,7 @@ $grey───────────────────────── # - G_WHIP_NOCANCEL=1 | Optional, hide the cancel button on inputbox, menu and checkbox dialogues # - G_WHIP_MENU_ARRAY | Required for G_WHIP_MENU to set available menu entries, 2 array indices per line: ('item' 'description') # - G_WHIP_CHECKLIST_ARRAY | Required for G_WHIP_CHECKLIST set available checklist options, 3 array indices per line: ('item' 'description' 'on'/'off') + # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags # Output: # - G_WHIP_RETURNED_VALUE | Returned value from inputbox/menu/checklist based whiptail items @@ -831,6 +832,7 @@ $grey───────────────────────── # G_WHIP_CHECKLIST "message" # - Prompt user to select multiple options from G_WHIP_CHECKLIST_ARRAY and save choice to G_WHIP_RETURNED_VALUE # - Exit code: 0=selection done, else=user cancelled or noninteractive + # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags G_WHIP_CHECKLIST() { local result=1 @@ -841,10 +843,48 @@ $grey───────────────────────── local WHIP_ERROR WHIP_BACKTITLE WHIP_SCROLLTEXT WHIP_SIZE_X WHIP_SIZE_Y WHIP_SIZE_Z WHIP_MESSAGE=$* NOCANCEL=() [[ $G_WHIP_NOCANCEL == 1 ]] && NOCANCEL=('--nocancel') G_WHIP_BUTTON_OK_TEXT=${G_WHIP_BUTTON_OK_TEXT:-Confirm} + + # Places to save original checklist and default + local orig_checklist=() original_default_item="$G_WHIP_DEFAULT_ITEM" + + #### + # If G_WHIP_CHECKLIST_ENUM=1, the checklist will be enumerated with numeric tags for whiptail, + # while preserving the original tags for mapping backwards after the checklist selection. + if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#G_WHIP_CHECKLIST_ARRAY[@]} -gt 0 ]] + then + # save the original checklist + orig_checklist=("${G_WHIP_CHECKLIST_ARRAY[@]}") + + # Build numeric-tagged checklist for whiptail while preserving original tags + local -a __tmp=() __idx=0 __map=() + for ((i=0;i<${#orig_checklist[@]};i+=3)); do + __map[$__idx]="${orig_checklist[$i]}" + __tmp+=("$__idx" "${orig_checklist[$i+1]}" "${orig_checklist[$i+2]}") + [[ -n "$original_default_item" && "$original_default_item" == "${orig_checklist[$i]}" ]] && G_WHIP_DEFAULT_ITEM=$__idx + ((__idx++)) + done + G_WHIP_CHECKLIST_ARRAY=("${__tmp[@]}") + fi + G_WHIP_INIT 3 # shellcheck disable=SC2086 G_WHIP_RETURNED_VALUE=$(whiptail ${G_PROGRAM_NAME:+--title "$G_PROGRAM_NAME"} --backtitle "$WHIP_BACKTITLE | Use spacebar to toggle selection" --checklist "$WHIP_MESSAGE" --separate-output --ok-button "$G_WHIP_BUTTON_OK_TEXT" --cancel-button "$G_WHIP_BUTTON_CANCEL_TEXT" "${NOCANCEL[@]}" --default-item "$G_WHIP_DEFAULT_ITEM" $WHIP_SCROLLTEXT "$WHIP_SIZE_Y" "$WHIP_SIZE_X" "$WHIP_SIZE_Z" -- "${G_WHIP_CHECKLIST_ARRAY[@]}" 3>&1 1>&2 2>&3-) result=$? + + # If enumeration was used, map numeric selections back to original tags + if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#orig_checklist[@]} -gt 0 && -n "$G_WHIP_RETURNED_VALUE" ]] + then + local sel mapped=() + for sel in $G_WHIP_RETURNED_VALUE; do + mapped+=("${__map[$sel]}") + done + G_WHIP_RETURNED_VALUE="${mapped[*]}" + # restore originals + G_WHIP_CHECKLIST_ARRAY=("${orig_checklist[@]}") + G_WHIP_DEFAULT_ITEM="$original_default_item" + unset -v __map __tmp __idx sel mapped + fi + G_WHIP_RETURNED_VALUE=$(echo -e "$G_WHIP_RETURNED_VALUE" | tr '\n' ' ') else G_WHIP_RETURNED_VALUE=$G_WHIP_DEFAULT_ITEM From 29418e5c0dced1ec745ea3856631984b4ed9a5a7 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 18:17:19 -0500 Subject: [PATCH 19/69] keys were set in 3 places, so made the defaults more automated/dynamic. housekeeping --- dietpi/func/dietpi-banner | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0a96b369c2..a02041674d 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -163,32 +163,35 @@ ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION ############## - # Set defaults: Disable CPU temp by default in VMs + ## Set default options + # Initialize aENABLED from aDESCRIPTION keys declare -gA aENABLED - if (( $G_HW_MODEL == 20 )) - then - aENABLED=( - [device_model]=1 [uptime]=0 [cpu_temp]=0 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=1 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 - ) - else - aENABLED=( - [device_model]=1 [uptime]=0 [cpu_temp]=1 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=0 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 - ) - fi - + for key in "${!aDESCRIPTION[@]}"; do + aENABLED[$key]=0 + done + # Set defaults: + aENABLED[device_model]=1 + aENABLED[lan_ip]=1 + aENABLED[disk_rootfs]=1 + aENABLED[useful_commands]=1 + aENABLED[motd]=1 + aENABLED[print_credits]=1 + # Enable CPU temp by default except in VMs + (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Default custom command example + ## Set default custom patterns and commands # shellcheck disable=SC2016 # Disable warning about single quotes in echo -e aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") - # Load user preferences, overriding the above defaults as needed + ## Load user preferences # Flag indicating whether a legacy numeric->associative migration happened during this run declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS # Derived convenience strings @@ -200,6 +203,7 @@ IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' IP4_re_cidr='([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}' + # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] then From 9a96bf8960182ef35424af10e9be4e9626817e91 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 30 Jun 2026 10:56:19 -0500 Subject: [PATCH 20/69] removed duplicate entry --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a02041674d..d69ec863da 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -148,7 +148,7 @@ declare -ga MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname \ lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status \ - network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap kernel + network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) # Keep a fixed index->key mapping to support numeric-index migration even if MENU_ITEMS changes later From 9325cc86258e0bae99ac9ca730275d338a5e7b2b Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 30 Jun 2026 15:13:30 -0500 Subject: [PATCH 21/69] cleaned up shellcheck failures --- dietpi/func/dietpi-banner | 6 +----- dietpi/func/dietpi-globals | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index d69ec863da..f800625c1c 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -757,15 +757,11 @@ $CLI_LINE" fi if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) - avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") + values_count=$((values_count+1)) else if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$used_kb" =~ ^[0-9]+$ ]]; then # If size and used are available, calculate available as size - used avail_kb=$((size_kb-used_kb)) - avail_gb=$(awk "BEGIN {printf \"%.1f \", ($avail_kb)/1024/1024}") - else - avail_gb="???" fi fi diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index f3a184092d..e85fb08620 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -848,7 +848,7 @@ $grey───────────────────────── local orig_checklist=() original_default_item="$G_WHIP_DEFAULT_ITEM" #### - # If G_WHIP_CHECKLIST_ENUM=1, the checklist will be enumerated with numeric tags for whiptail, + # If G_WHIP_CHECKLIST_ENUM=1, the checklist will be enumerated with numeric tags for whiptail, # while preserving the original tags for mapping backwards after the checklist selection. if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#G_WHIP_CHECKLIST_ARRAY[@]} -gt 0 ]] then From b9d6506f09eb717fddd4af9c4e233f0916e7c447 Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 1 Jul 2026 11:51:11 -0500 Subject: [PATCH 22/69] added migration for legacy aCOLOUR preferences --- dietpi/func/dietpi-banner | 64 ++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index f800625c1c..6b5faeac81 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -114,6 +114,44 @@ unset _tmp_enabled fi fi + + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form + # Only perform migration when numeric indices exist in the loaded aCOLOUR + if declare -p aCOLOUR >/dev/null 2>&1; then + local has_numeric_col=0 + for k in "${!aCOLOUR[@]}"; do + if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric_col=1; break; fi + done + if (( has_numeric_col )); then + # Prefer LEGACY_INDEXED_COLOUR_KEYS for a stable mapping + local -n key_map_ref_col=LEGACY_INDEXED_COLOUR_KEYS + declare -gA _tmp_colour=() + # First, map numeric indices explicitly so legacy numeric prefs take precedence + for (( i=0; i<${#key_map_ref_col[@]}; i++ )); do + if [[ -n "${aCOLOUR[$i]+x}" ]]; then + key=${key_map_ref_col[$i]} + [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$i]:-} + fi + done + # Then copy any remaining non-numeric keys that weren't set by numeric mapping + for idx in "${!aCOLOUR[@]}"; do + if [[ ! "$idx" =~ ^[0-9]+$ ]]; then + key=$idx + # don't overwrite keys already set by numeric mapping + [[ -n "${_tmp_colour[$key]+x}" ]] && continue + [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$idx]:-} + fi + done + # replace aCOLOUR with associative mapping + unset aCOLOUR + declare -gA aCOLOUR + for k in "${!_tmp_colour[@]}"; do aCOLOUR[$k]=${_tmp_colour[$k]}; done + + # Mark that a colour migration from legacy numeric indices occurred during this run + declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 + unset _tmp_colour + fi + fi } # Use associative arrays keyed by short names derived from the description @@ -151,16 +189,21 @@ network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) - # Keep a fixed index->key mapping to support numeric-index migration even if MENU_ITEMS changes later - # LEGACY_INDEXED_BANNER_KEYS maps legacy numeric indices to the current key names (0 -> device_model, 1 -> uptime, ...) ############## - ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. ############## + # Fixed mapping for legacy numeric aENABLED indices -> associative keys declare -ga LEGACY_INDEXED_BANNER_KEYS=( device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status ) + # Fixed mapping for legacy numeric aCOLOUR indices -> associative keys + declare -ga LEGACY_INDEXED_COLOUR_KEYS=( + ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT + ) ############## - ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. ############## ## Set default options @@ -189,8 +232,10 @@ aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") ## Load user preferences - # Flag indicating whether a legacy numeric->associative migration happened during this run + # Flag indicating whether a legacy numeric->associative preference migration happened during this run declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + # Flag indicating whether a legacy numeric->associative colour migration happened during this run + declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS @@ -304,6 +349,7 @@ # Clear migration flag now that prefs are persisted DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 return 0 } @@ -860,8 +906,10 @@ $CLI_LINE" Print_Updates (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands - # Notify user if prefs were migrated from legacy numeric indices during this run - (( DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + # Notify user if prefs or colors were migrated from legacy numeric indices during this run + if (( DIETPI_BANNER_MIGRATED_DURING_RUN )) || (( DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN )); then + echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + fi } Print_Banner() @@ -1014,7 +1062,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Before saving, create a backup of the existing prefs if migrating. # FP_SAVEFILE gets truncated when calling Save() below, so we need to backup first. - if [[ -f "$FP_SAVEFILE" ]] && [[ $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]]; then + if [[ -f "$FP_SAVEFILE" ]] && { [[ $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]] || [[ $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN == 1 ]]; }; then backup="$FP_SAVEFILE.bak-$(date -Iseconds)" cp -a "$FP_SAVEFILE" "$backup" || true fi From 851a63b4e3dfeb669cdc2ab20d93c790817e6de2 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sat, 4 Jul 2026 07:59:58 -0500 Subject: [PATCH 23/69] added error handling for No IPs blocked (or Fail2ban not running) --- dietpi/func/dietpi-banner | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 6b5faeac81..00c09de967 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -533,7 +533,12 @@ $CLI_LINE" # get banned IP address count IP_addrs="$(echo "$f2b_output" | grep -o '\[[^]]*\]' | grep -Eo "$IP4_re" | sort -u)" - count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" + if [[ -z "$IP_addrs" ]]; then + IP_addrs="" + count_banned_ips=0 + else + count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" + fi echo "$count_banned_ips" return 0 @@ -550,7 +555,7 @@ $CLI_LINE" 0) if [[ $count_banned_ips -eq 0 ]]; then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" else if [[ $count_banned_ips -gt 20 ]]; then # high number of bans @@ -717,7 +722,7 @@ $CLI_LINE" output="$(eval "$cmd" 2>&1)" status=$? if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$((i))] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -820,8 +825,8 @@ $CLI_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ - "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" \ + "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } @@ -1083,4 +1088,4 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" #----------------------------------------------------------------------------------- exit 0 #----------------------------------------------------------------------------------- -} \ No newline at end of file +} From 3ed1dc3b41450047b85c86495433d680ccea0d0f Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 16:24:53 +0200 Subject: [PATCH 24/69] dietpi-banner: code alignments and simplification * align indentation * use "declare" only where needed (for assiciative arrays) * use "declare -g" only where needed: when defining global variables inside a function * use quotation in bash conditionals and variable assignment only where needed * use single-quotes consistently if no variable expansion or literal `'` is needed * prefer $EUID over external `id -u` call * prefer mawk over awk (which can be the slower gawk if installed) * prefer bash arithmetic context over `-ge` and alike operators * skip `|| true` as no `set -e` is used * merge bash conditionals where possible * simplify numeric => assiciative config array conversion: alter the new arrays in place instead of using a temporary conversion array: Both arrays are declared as assiciative arrays already before the config is sourced. Hence they remain assiciative arrays, just with the numberic indices added. If any index matches an existing old numeric key, it can be applied as its new assiciative key directly, and the numeric value unset. The mapping arrays were inlined, so ultimately this code can go into dietpi-update patches instead. --- dietpi/func/dietpi-banner | 514 +++++++++++++++++--------------------- 1 file changed, 229 insertions(+), 285 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 00c09de967..b0a3ef19c6 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -59,15 +59,15 @@ { declare -gA aCOLOUR=( # Default colors - [ACCENT]='\e[38;5;154m' # DietPi green | Lines, bullets and separators - [STRONG]='\e[1m' # Bold white | Main descriptions - [WEAK]='\e[90m' # Grey | Subdued text - [ALERT]='\e[91m' # Red | Update notifications / Bad state - [GOOD]='\e[1;32m' # Green | Good state - [HIGHLIGHT]='\e[1;33m' # Yellow | Warning state - [ALT]='\e[1;36m' # Blue | Dynamic match + [ACCENT]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [STRONG]='\e[1m' # Bold white | Main descriptions + [WEAK]='\e[90m' # Grey | Subdued text + [ALERT]='\e[91m' # Red | Update notifications / Bad state + [GOOD]='\e[1;32m' # Green | Good state + [HIGHLIGHT]='\e[1;33m' # Yellow | Warning state + [ALT]='\e[1;36m' # Blue | Dynamic match ) - declare -g COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) + COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) # Load settings here, to have chosen custom CLI colors applied # disable shellcheck for non-constant `source` (`.`) @@ -75,87 +75,45 @@ [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form - # Only perform migration when numeric indices exist in the loaded aENABLED - if declare -p aENABLED >/dev/null 2>&1; then - local has_numeric=0 - for k in "${!aENABLED[@]}"; do - if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric=1; break; fi - done - if (( has_numeric )); then - # Prefer LEGACY_INDEXED_BANNER_KEYS for a stable mapping; fallback to MENU_ITEMS - local -n key_map_ref=LEGACY_INDEXED_BANNER_KEYS - if [[ ${#LEGACY_INDEXED_BANNER_KEYS[@]} -eq 0 && ${#MENU_ITEMS[@]} -gt 0 ]]; then - key_map_ref=MENU_ITEMS - fi - declare -gA _tmp_enabled=() - # First, map numeric indices explicitly so legacy numeric prefs take precedence - for (( i=0; i<${#key_map_ref[@]}; i++ )); do - if [[ -n "${aENABLED[$i]+x}" ]]; then - key=${key_map_ref[$i]} - [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$i]:-0} - fi - done - # Then copy any remaining non-numeric keys that weren't set by numeric mapping - for idx in "${!aENABLED[@]}"; do - if [[ ! "$idx" =~ ^[0-9]+$ ]]; then - key=$idx - # don't overwrite keys already set by numeric mapping - [[ -n "${_tmp_enabled[$key]+x}" ]] && continue - [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$idx]:-0} - fi - done - # replace aENABLED with associative mapping - unset aENABLED - declare -gA aENABLED - for k in "${!_tmp_enabled[@]}"; do aENABLED[$k]=${_tmp_enabled[$k]}; done - - # Mark that a migration from legacy numeric indices occurred during this run - declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=1 - unset _tmp_enabled - fi - fi + local k legacy_indexed_enabled_keys=( + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd + vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status + ) + for k in "${!aENABLED[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_enabled_keys if there is one + [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue + + # Unset old numeric index + unset -v "aENABLED[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form - # Only perform migration when numeric indices exist in the loaded aCOLOUR - if declare -p aCOLOUR >/dev/null 2>&1; then - local has_numeric_col=0 - for k in "${!aCOLOUR[@]}"; do - if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric_col=1; break; fi - done - if (( has_numeric_col )); then - # Prefer LEGACY_INDEXED_COLOUR_KEYS for a stable mapping - local -n key_map_ref_col=LEGACY_INDEXED_COLOUR_KEYS - declare -gA _tmp_colour=() - # First, map numeric indices explicitly so legacy numeric prefs take precedence - for (( i=0; i<${#key_map_ref_col[@]}; i++ )); do - if [[ -n "${aCOLOUR[$i]+x}" ]]; then - key=${key_map_ref_col[$i]} - [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$i]:-} - fi - done - # Then copy any remaining non-numeric keys that weren't set by numeric mapping - for idx in "${!aCOLOUR[@]}"; do - if [[ ! "$idx" =~ ^[0-9]+$ ]]; then - key=$idx - # don't overwrite keys already set by numeric mapping - [[ -n "${_tmp_colour[$key]+x}" ]] && continue - [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$idx]:-} - fi - done - # replace aCOLOUR with associative mapping - unset aCOLOUR - declare -gA aCOLOUR - for k in "${!_tmp_colour[@]}"; do aCOLOUR[$k]=${_tmp_colour[$k]}; done - - # Mark that a colour migration from legacy numeric indices occurred during this run - declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 - unset _tmp_colour - fi - fi + local legacy_indexed_colour_keys=(ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT) + for k in "${!aCOLOUR[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_colour_keys if there is one + [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue + + # Unset old numeric index + unset -v "aCOLOUR[$k]" + + # Mark that a colour migration from legacy numeric indices occurred during this run + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 + done } # Use associative arrays keyed by short names derived from the description - declare -gA aDESCRIPTION=( + declare -A aDESCRIPTION=( [device_model]='Device model' [uptime]='Uptime' [cpu_temp]='CPU temp' @@ -183,36 +141,20 @@ [fail2ban_status]='Fail2Ban Status' ) # Set the array order for the menu, items must be listed here to show up - declare -ga MENU_ITEMS=( + MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname \ lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status \ network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) - ############## - ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION - ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. - ############## - # Fixed mapping for legacy numeric aENABLED indices -> associative keys - declare -ga LEGACY_INDEXED_BANNER_KEYS=( - device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status - ) - # Fixed mapping for legacy numeric aCOLOUR indices -> associative keys - declare -ga LEGACY_INDEXED_COLOUR_KEYS=( - ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT - ) - ############## - ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION - ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. - ############## - ## Set default options # Initialize aENABLED from aDESCRIPTION keys - declare -gA aENABLED - for key in "${!aDESCRIPTION[@]}"; do - aENABLED[$key]=0 + declare -A aENABLED + for k in "${!aDESCRIPTION[@]}" + do + aENABLED[$k]=0 done - # Set defaults: + # Set defaults aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 @@ -233,9 +175,9 @@ ## Load user preferences # Flag indicating whether a legacy numeric->associative preference migration happened during this run - declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_MIGRATED_DURING_RUN=0 # Flag indicating whether a legacy numeric->associative colour migration happened during this run - declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS @@ -308,9 +250,10 @@ Save() { # Persist associative aENABLED as simple assignments (one per line) - for key in "${MENU_ITEMS[@]}"; do - val=${aENABLED[$key]:-0} - printf "aENABLED[%s]=%s\n" "$key" "$val" + for k in "${MENU_ITEMS[@]}" + do + val=${aENABLED[$k]:-0} + printf 'aENABLED[%s]=%s\n' "$k" "$val" done # Persist only the primary colour slots. Convert any actual @@ -319,9 +262,9 @@ for i in "${!aCOLOUR[@]}" do # Don't save the RESET colour slot - [[ "$i" == "RESET" ]] && continue + [[ $i == 'RESET' ]] && continue - val="${aCOLOUR[$i]}" + val=${aCOLOUR[$i]} # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') # Escape single quotes for safe embedding @@ -330,14 +273,16 @@ done # Persist aDISK_SPACE_PATTERNS (indices 0..N) - for i in "${!aDISK_SPACE_PATTERNS[@]}"; do + for i in "${!aDISK_SPACE_PATTERNS[@]}" + do patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" done # Persist aCUSTOM_COMMANDS (indices 0..N) - for i in "${!aCUSTOM_COMMANDS[@]}"; do + for i in "${!aCUSTOM_COMMANDS[@]}" + do cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" @@ -410,16 +355,16 @@ $CLI_LINE" do # disable shellcheck for 'require-double-brackets' so it is `dash` compatible # shellcheck disable=SC2292 - [ -f "$i" ] || continue + [[ -f $i ]] || continue openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' return 0 done return 1 } - stat="" + stat= code=0 - if [[ "$(id -u)" = "0" ]] + if [[ $EUID == 0 ]] then # Running as root stat=$(Get_Cert_Status_Raw 2>/dev/null) @@ -432,15 +377,16 @@ $CLI_LINE" case $code in 0) - case "$stat" in - Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}" ;; - No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}" ;; + # stat is valid + case $stat in + Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}";; + No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}";; + *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}";; esac - ;; # stat is valid - 1) stat="${aCOLOUR[ALERT]}No certificate found${COLOUR_RESET}" ;; - 2) stat="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[HIGHLIGHT]}Unable to obtain cert status${COLOUR_RESET}" ;; + ;; + 1) stat="${aCOLOUR[ALERT]}No certificate found${COLOUR_RESET}";; + 2) stat="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required${COLOUR_RESET}";; + *) stat="${aCOLOUR[HIGHLIGHT]}Unable to obtain cert status${COLOUR_RESET}";; esac echo "$stat" @@ -495,19 +441,19 @@ $CLI_LINE" { # Print a single line item with green bullet, description "$1", green separator and state "$2" # state should contain color and format codes as needed - local subtitle="$1" - local state="$2" - local width="${3:-}" - local color="${aCOLOUR[STRONG]}" - printf "%b%b %-${width}b %b %b\n" \ - "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" + local subtitle=$1 + local state=$2 + local width=${3:-} + local color=${aCOLOUR[STRONG]} + printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" } Get_Systemd_Status() { # Print systemd overall status based on "systemctl --failed" - FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2>/dev/null || true) - if [[ -z "$FAILED_CNT" || "$FAILED_CNT" -eq 0 ]]; then + FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) + if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] + then state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" else state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" @@ -519,22 +465,26 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - if command -v fail2ban-client >/dev/null 2>&1; then - if [[ "$(id -u)" = "0" ]]; then + if command -v fail2ban-client >/dev/null 2>&1 + then + if [[ $EUID == 0 ]] + then f2b_output="$(fail2ban-client banned 2>/dev/null)" else f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" - [[ $? = 2 ]] && return 2 + [[ $? == 2 ]] && return 2 fi - if ! command -v sort >/dev/null 2>&1; then + if ! command -v sort >/dev/null 2>&1 + then sort() { cat; } # fallback if sort is not available, simply passes input through fi # get banned IP address count IP_addrs="$(echo "$f2b_output" | grep -o '\[[^]]*\]' | grep -Eo "$IP4_re" | sort -u)" - if [[ -z "$IP_addrs" ]]; then - IP_addrs="" + if [[ ! "$IP_addrs" ]] + then + IP_addrs= count_banned_ips=0 else count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" @@ -553,11 +503,13 @@ $CLI_LINE" case $code in 0) - if [[ $count_banned_ips -eq 0 ]]; then + if [[ $count_banned_ips -eq 0 ]] + then # zero bans can be suspicious - something may not be working correctly state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" else - if [[ $count_banned_ips -gt 20 ]]; then + if [[ $count_banned_ips -gt 20 ]] + then # high number of bans state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else @@ -565,16 +517,10 @@ $CLI_LINE" state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi - ;; - 1) - state="${aCOLOUR[ALERT]}fail2ban-client not available${COLOUR_RESET}" - ;; - 2) - state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" - ;; - *) - state="${aCOLOUR[ALERT]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}" - ;; + ;; + 1) state="${aCOLOUR[ALERT]}fail2ban-client not available${COLOUR_RESET}";; + 2) state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}";; + *) state="${aCOLOUR[ALERT]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}";; esac echo "$state" @@ -589,7 +535,7 @@ $CLI_LINE" { # Defaults local timeout=3 - local netns="" + local netns # Inputs while (( $# )) do @@ -601,12 +547,11 @@ $CLI_LINE" shift done - local -a ns_cmd_prefix=() - if [[ -n "$netns" ]]; then - if [[ "$(id -u)" -ne "0" ]]; then - # add sudo if not root - ns_cmd_prefix+=(sudo -n) - fi + local ns_cmd_prefix=() + if [[ $netns ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi @@ -619,95 +564,92 @@ $CLI_LINE" # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) local err_re='timed out|timeout|could not resolve|not known|unknown' - DATA_USAGE(){ - local netns="$1" + DATA_USAGE() + { + local netns=$1 # Use an array prefix so we can run commands either in a netns # (ip netns exec ...) or the default namespace, and with sudo if needed - local -a ns_cmd_prefix=() - if [[ -n "$netns" ]]; then - if [[ "$(id -u)" -ne "0" ]]; then - # add sudo if not root - ns_cmd_prefix+=(sudo -n) - fi + local ns_cmd_prefix=() + if [[ $netns ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | mawk -F': ' '{print $2}') - for IFACE in "${observed_ifaces[@]}"; do + for IFACE in "${observed_ifaces[@]}" + do # strip kernel-style peer suffix (e.g. eth0@if8) and use base name local ifname="${IFACE%%@*}" # skip loopback - [[ "$ifname" == lo ]] && continue + [[ $ifname == 'lo' ]] && continue - case "$ifname" in + case $ifname in # wired / predictable names - eth*|en*|lan*) : ;; + eth*|en*|lan*) :;; # wireless - wlan*|wl*) : ;; + wlan*|wl*) :;; # vpn/tunnel/virtual - tun*|wg*|vpn*) : ;; + tun*|wg*|vpn*) :;; *) continue ;; esac - local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr # Get RX data - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get TX data - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) - [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=" - No IP - " - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") - [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + ip_addr=' - No IP - ' + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | mawk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") + [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then - printf "%s %s%-20s %s%s TX= %11s RX= %11s%s\n" \ - "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" - fi + [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue + printf '%s %s%-20s %s%s TX= %11s RX= %11s%s\n' "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" done } - mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') + mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 { print $1 }') # Prepend an empty entry so the default (root) namespace can be handled in the loop - namespaces=( "" "${namespaces[@]}" ) + namespaces=('' "${namespaces[@]}") - for ns in "${namespaces[@]}"; do - raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 24)" + for ns in "${namespaces[@]}" + do + raw_ns=${ns:-default} + display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) - if [[ $IP =~ $IP4_re ]]; then + if [[ $IP =~ $IP4_re ]] + then # Valid IP detected, keep as-is and print with namespace # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. # IP="${BASH_REMATCH[0]}" - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" - elif [[ -z $IP ]]; then + elif [[ ! $IP ]] + then # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. - printf "%s %s %s %s%s%s\n" \ - "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" + printf '%s %s %s %s%s%s\n' "${aCOLOUR[ALERT]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" else - if [[ $IP =~ $err_re ]]; then - # Treat known curl/network error messages as timeout/DNS failures - IP="Timeout/DNS Failure" - fi + # Treat known curl/network error messages as timeout/DNS failures + [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' # If the IP contains an unknown or error message, print it with the namespace - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -716,16 +658,16 @@ $CLI_LINE" Print_Custom_Commands() { - for i in "${!aCUSTOM_COMMANDS[@]}"; do - local cmd="${aCUSTOM_COMMANDS[$i]}" - if [[ -n "$cmd" ]]; then - output="$(eval "$cmd" 2>&1)" - status=$? - if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$((i))] Exit Code: ${status}${COLOUR_RESET}" - else - printf '%b\n' "$output" - fi + for i in "${!aCUSTOM_COMMANDS[@]}" + do + [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue + output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) + status=$? + if (( $status )) + then + printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" + else + printf '%b\n' "$output" fi done } @@ -733,22 +675,24 @@ $CLI_LINE" Get_Disk_Usage() { # If no arguments are provided, default to '"/" "/mnt/*"'. - if (( $# > 0 )); then - aDISK_SPACE_PATTERNS=("$@") - fi + (( $# )) && aDISK_SPACE_PATTERNS=("$@") # TODO: is there a more efficient reader than df (stat?) # Use df's output to parse numeric fields (kB units) for mounts matching the supplied patterns length=0 results=() - while read -r size_kb used_kb avail_kb mnt_target; do + while read -r size_kb used_kb avail_kb mnt_target + do matched=0 basename= - for pattern in "${aDISK_SPACE_PATTERNS[@]}"; do + for pattern in "${aDISK_SPACE_PATTERNS[@]}" + do # If the user supplies a pattern prefixed with 're:' treat it as regex - if [[ "$pattern" == re:* ]]; then - re="${pattern#re:}" - if [[ $mnt_target =~ $re ]]; then + if [[ $pattern == 're:'* ]] + then + re=${pattern#re:} + if [[ $mnt_target =~ $re ]] + then matched=1 basename=$(basename "$mnt_target") break @@ -761,83 +705,86 @@ $CLI_LINE" matched=1 basename=$(basename "$mnt_target") break - ;; - *) ;; + ;; + *) :;; esac fi done - if (( matched )); then + if (( $matched )) + then len=${#basename} - if (( len > length )); then - length=$len - fi + (( $len > $length )) && length=$len results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") fi done < <(df -k --output=size,used,avail,target | tail -n +2) # Print results with aligned columns, converting kB to GB and calculating percentage used - for entry in "${results[@]}"; do + for entry in "${results[@]}" + do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" local values_count=0 # Do all math in kB, convert to GB for display and round to 1 decimal - if [[ "$size_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) - size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + if [[ $size_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) + size_gb=$(mawk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") else - if [[ "$used_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + if [[ $used_kb =~ ^[0-9]+$ && $avail_kb =~ ^[0-9]+$ ]] + then # If used and available are available, calculate size as used + available - size_kb=$((used_kb+avail_kb)) - size_gb=$(awk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") + size_kb=$(( $used_kb + $avail_kb )) + size_gb=$(mawk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") else - size_gb="???" + size_gb='???' fi fi - if [[ "$used_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) - used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + if [[ $used_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) + used_gb=$(mawk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") else - if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + if [[ $size_kb =~ ^[0-9]+$ && $avail_kb =~ ^[0-9]+$ ]] + then # If size and available are available, calculate used as size - available - used_kb=$((size_kb-avail_kb)) - used_gb=$(awk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") + used_kb=$(( $size_kb - $avail_kb )) + used_gb=$(mawk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") else - used_gb="???" + used_gb='???' fi fi - if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) + if [[ $avail_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) else - if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$used_kb" =~ ^[0-9]+$ ]]; then - # If size and used are available, calculate available as size - used - avail_kb=$((size_kb-used_kb)) - fi + # If size and used are available, calculate available as size - used + [[ $size_kb =~ ^[0-9]+$ && $used_kb =~ ^[0-9]+$ ]] && avail_kb=$(( $size_kb - $used_kb )) fi # Calculate percentage used based on available data: - if [[ "$values_count" -ge 2 ]]; then + if (( $values_count > 1 )) + then # this should match the `df` output - perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + perc=$(mawk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") else - perc="?? %" # default unknown value + perc='?? %' # default unknown value fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" \ - "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } Get_VPN_Status() { - local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" - case "$state" in - Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}" ;; - Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}" ;; - *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}" ;; + local state=$(/boot/dietpi/dietpi-vpn status 2>&1) + case $state in + Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}";; + Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}";; + *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}";; esac echo "$state" } @@ -848,7 +795,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( "$FP_SAVEFILE" From 5d3ccc2acd45371fa93faba503ca0e1c7c715aef Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 17:07:28 +0200 Subject: [PATCH 25/69] dietpi-banner: some restructure * some reordering to reduce the diff * use lower-case colour array keys * merge the two unnecessarily split array migration flags * some more cleanup --- dietpi/func/dietpi-banner | 287 ++++++++++++++++++-------------------- 1 file changed, 138 insertions(+), 149 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index b0a3ef19c6..ae4ab8fed0 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -23,8 +23,6 @@ INPUT=$* # Import DietPi-Globals -------------------------------------------------------------- - # disable shellcheck for non-constant `source` (`.`) - # shellcheck disable=SC1090 . /boot/dietpi/func/dietpi-globals # - Allow concurrent banner prints but a single menu call only if [[ ! $INPUT ]] @@ -44,74 +42,6 @@ readonly FP_SAVEFILE='/boot/dietpi/.dietpi-banner' readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' - # ---------------------------------------------------------------------------- - # Custom preferences initialisation helper. It loads the user-saved settings file - # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. - # Color and format references: - # `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` - # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" - # `https://www.ditig.com/256-colors-cheat-sheet` - # `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` - # - # Usage: `DIETPI_BANNER_LOAD_PREFS` - # ---------------------------------------------------------------------------- - DIETPI_BANNER_LOAD_PREFS() - { - declare -gA aCOLOUR=( - # Default colors - [ACCENT]='\e[38;5;154m' # DietPi green | Lines, bullets and separators - [STRONG]='\e[1m' # Bold white | Main descriptions - [WEAK]='\e[90m' # Grey | Subdued text - [ALERT]='\e[91m' # Red | Update notifications / Bad state - [GOOD]='\e[1;32m' # Green | Good state - [HIGHLIGHT]='\e[1;33m' # Yellow | Warning state - [ALT]='\e[1;36m' # Blue | Dynamic match - ) - COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) - - # Load settings here, to have chosen custom CLI colors applied - # disable shellcheck for non-constant `source` (`.`) - # shellcheck disable=SC1090 - [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" - - # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form - local k legacy_indexed_enabled_keys=( - device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd - vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status - ) - for k in "${!aENABLED[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_enabled_keys if there is one - [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue - - # Unset old numeric index - unset -v "aENABLED[$k]" - - # Mark that a migration from legacy numeric indices occurred during this run - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - done - - # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form - local legacy_indexed_colour_keys=(ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT) - for k in "${!aCOLOUR[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_colour_keys if there is one - [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue - - # Unset old numeric index - unset -v "aCOLOUR[$k]" - - # Mark that a colour migration from legacy numeric indices occurred during this run - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 - done - } - # Use associative arrays keyed by short names derived from the description declare -A aDESCRIPTION=( [device_model]='Device model' @@ -142,19 +72,19 @@ ) # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( - large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname \ - lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status \ + large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname + lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) - ## Set default options # Initialize aENABLED from aDESCRIPTION keys declare -A aENABLED for k in "${!aDESCRIPTION[@]}" do aENABLED[$k]=0 done - # Set defaults + + # Set default options aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 @@ -163,28 +93,93 @@ aENABLED[print_credits]=1 # Enable CPU temp by default except in VMs (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 + + # Default colours + COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) + # Colour and format references: + # - `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` + # - `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" + # - `https://www.ditig.com/256-colors-cheat-sheet` + # - `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` + declare -A aCOLOUR=( + [accent]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [strong]='\e[1m' # Bold white | Main descriptions + [weak]='\e[90m' # Grey | Subdued text + [alert]='\e[91m' # Red | Update notifications / Bad state + [good]='\e[1;32m' # Green | Good state + [highlight]='\e[1;33m' # Yellow | Warning state + [alt]='\e[1;36m' # Blue | Dynamic match + ) + # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - ## Set default custom patterns and commands - # shellcheck disable=SC2016 # Disable warning about single quotes in echo -e - aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' + # Set default custom patterns and commands + # shellcheck disable=SC2016 + aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"' # Default disk patterns - aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") + aDISK_SPACE_PATTERNS=('/' '/mnt/dietpi_userdata') - ## Load user preferences - # Flag indicating whether a legacy numeric->associative preference migration happened during this run + # Load settings here, to have chosen custom CLI colours applied + # shellcheck disable=SC1090 + [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + + # ---------------------------------------------------------------------------- + # Legacy config migration helper + # ---------------------------------------------------------------------------- + # Migration helper function + DIETPI_BANNER_LEGACY_MIGRATION() + { + # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form + local k legacy_indexed_enabled_keys=( + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd + vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status + ) + for k in "${!aENABLED[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_enabled_keys if there is one + # shellcheck disable=SC2015 + [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue + + # Unset old numeric index + unset -v "aENABLED[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done + + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form + local legacy_indexed_colour_keys=(accent strong weak alert good highlight alt) + for k in "${!aCOLOUR[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_colour_keys if there is one + # shellcheck disable=SC2015 + [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue + + # Unset old numeric index + unset -v "aCOLOUR[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done + } + # Flag indicating whether a legacy numeric->associative config migration happened during this run DIETPI_BANNER_MIGRATED_DURING_RUN=0 - # Flag indicating whether a legacy numeric->associative colour migration happened during this run - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 - # Override the above defaults as needed - DIETPI_BANNER_LOAD_PREFS + # Run migration + DIETPI_BANNER_LEGACY_MIGRATION + # ---------------------------------------------------------------------------- # Derived convenience strings - CLI_LINE=" ${aCOLOUR[ACCENT]}─────────────────────────────────────────────────────$COLOUR_RESET" - CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" - CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" + CLI_LINE=" ${aCOLOUR[accent]}─────────────────────────────────────────────────────$COLOUR_RESET" + CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" + CLI_SEPARATOR="${aCOLOUR[accent]}:$COLOUR_RESET" # IP address detection (IPv4) IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' @@ -250,10 +245,9 @@ Save() { # Persist associative aENABLED as simple assignments (one per line) - for k in "${MENU_ITEMS[@]}" + for i in "${!aDESCRIPTION[@]}" do - val=${aENABLED[$k]:-0} - printf 'aENABLED[%s]=%s\n' "$k" "$val" + echo "aENABLED[$i]=${aENABLED[$i]}" done # Persist only the primary colour slots. Convert any actual @@ -261,9 +255,6 @@ # portable when sourced in different shells. for i in "${!aCOLOUR[@]}" do - # Don't save the RESET colour slot - [[ $i == 'RESET' ]] && continue - val=${aCOLOUR[$i]} # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') @@ -277,7 +268,7 @@ do patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} - printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" + echo "aDISK_SPACE_PATTERNS[$i]='$patt'" done # Persist aCUSTOM_COMMANDS (indices 0..N) @@ -285,7 +276,7 @@ do cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} - printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" + echo "aCUSTOM_COMMANDS[$i]='$cmd'" done # Persist BW indent settings @@ -294,8 +285,6 @@ # Clear migration flag now that prefs are persisted DIETPI_BANNER_MIGRATED_DURING_RUN=0 - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 - return 0 } Print_Header() @@ -303,29 +292,29 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[HIGHLIGHT]}Update available" + local text_update_available_date="${aCOLOUR[highlight]}Update available" # New DietPi live patches available? elif Check_DietPi_Live_Patches then - local text_update_available_date="${aCOLOUR[HIGHLIGHT]}New live patches available" + local text_update_available_date="${aCOLOUR[highlight]}New live patches available" # APT update available? elif Check_APT_Updates then - local text_update_available_date="${aCOLOUR[HIGHLIGHT]}$PACKAGE_COUNT APT updates available" + local text_update_available_date="${aCOLOUR[highlight]}$PACKAGE_COUNT APT updates available" # Reboot required to finalise kernel upgrade? elif Check_Reboot then - local text_update_available_date="${aCOLOUR[ALERT]}Reboot required" + local text_update_available_date="${aCOLOUR[alert]}Reboot required" else local locale=$(sed -n '/^[[:blank:]]*AUTO_SETUP_LOCALE=/{s/^[^=]*=//p;q}' /boot/dietpi.txt) local text_update_available_date=$(LC_ALL=${locale:-C.UTF-8} date '+%R - %a %x') fi echo -e "$CLI_LINE - ${aCOLOUR[STRONG]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET + ${aCOLOUR[strong]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET $CLI_LINE" } @@ -333,7 +322,7 @@ $CLI_LINE" { # ON THE CURRENT LINE Output green line separator with subheader title "$1" echo -e "\r$CLI_LINE - ${aCOLOUR[STRONG]}$1 $CLI_SEPARATOR" + ${aCOLOUR[strong]}$1 $CLI_SEPARATOR" } Get_Local_Ip() @@ -379,14 +368,14 @@ $CLI_LINE" 0) # stat is valid case $stat in - Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}";; - No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}";; - *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}";; + Valid*) stat="${aCOLOUR[weak]}$stat${COLOUR_RESET}";; + No\ certificate*) stat="${aCOLOUR[alert]}$stat${COLOUR_RESET}";; + *) stat="${aCOLOUR[highlight]}$stat${COLOUR_RESET}";; esac ;; - 1) stat="${aCOLOUR[ALERT]}No certificate found${COLOUR_RESET}";; - 2) stat="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required${COLOUR_RESET}";; - *) stat="${aCOLOUR[HIGHLIGHT]}Unable to obtain cert status${COLOUR_RESET}";; + 1) stat="${aCOLOUR[alert]}No certificate found${COLOUR_RESET}";; + 2) stat="${aCOLOUR[highlight]}NOPASSWD sudo required${COLOUR_RESET}";; + *) stat="${aCOLOUR[highlight]}Unable to obtain cert status${COLOUR_RESET}";; esac echo "$stat" @@ -394,7 +383,7 @@ $CLI_LINE" Print_Credits() { - echo -e " ${aCOLOUR[WEAK]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + echo -e " ${aCOLOUR[weak]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info @@ -409,32 +398,32 @@ $CLI_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" # New DietPi live patches available? elif (( $LIVE_PATCHES )) then - echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" # APT updates available? elif (( $PACKAGE_COUNT )) then - echo -e " ${aCOLOUR[STRONG]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + echo -e " ${aCOLOUR[strong]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" # Reboot required to finalise kernel upgrade? elif (( $REBOOT_REQUIRED )) then - echo -e " ${aCOLOUR[STRONG]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[ALERT]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + echo -e " ${aCOLOUR[strong]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[alert]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" fi } Print_Useful_Commands() { - echo -e " ${aCOLOUR[STRONG]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[STRONG]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[STRONG]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation - ${aCOLOUR[STRONG]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor - ${aCOLOUR[STRONG]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" + echo -e " ${aCOLOUR[strong]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[strong]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[strong]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation + ${aCOLOUR[strong]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor + ${aCOLOUR[strong]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" } Print_Item_State() @@ -444,7 +433,7 @@ $CLI_LINE" local subtitle=$1 local state=$2 local width=${3:-} - local color=${aCOLOUR[STRONG]} + local color=${aCOLOUR[strong]} printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" } @@ -454,9 +443,9 @@ $CLI_LINE" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] then - state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" + state="${aCOLOUR[weak]}No Services Failed${COLOUR_RESET}" else - state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + state="${aCOLOUR[alert]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" fi echo "$state" } @@ -506,21 +495,21 @@ $CLI_LINE" if [[ $count_banned_ips -eq 0 ]] then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" + state="${aCOLOUR[highlight]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" else if [[ $count_banned_ips -gt 20 ]] then # high number of bans - state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[alert]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[weak]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi ;; - 1) state="${aCOLOUR[ALERT]}fail2ban-client not available${COLOUR_RESET}";; - 2) state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}";; - *) state="${aCOLOUR[ALERT]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}";; + 1) state="${aCOLOUR[alert]}fail2ban-client not available${COLOUR_RESET}";; + 2) state="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}";; + *) state="${aCOLOUR[alert]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}";; esac echo "$state" @@ -615,7 +604,7 @@ $CLI_LINE" [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - printf '%s %s%-20s %s%s TX= %11s RX= %11s%s\n' "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" + printf '%s %s%-20s %s%s TX= %11s RX= %11s%s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" "$COLOUR_RESET" done } @@ -637,19 +626,19 @@ $CLI_LINE" # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. # IP="${BASH_REMATCH[0]}" - printf ' %s%s [%s]%s\n' "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" elif [[ ! $IP ]] then # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. - printf '%s %s %s %s%s%s\n' "${aCOLOUR[ALERT]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" + printf '%s %s %s %s%s%s\n' "${aCOLOUR[alert]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$display_ns" "$COLOUR_RESET" else # Treat known curl/network error messages as timeout/DNS failures [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' # If the IP contains an unknown or error message, print it with the namespace - printf ' %s%s [%s]%s\n' "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[highlight]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -665,7 +654,7 @@ $CLI_LINE" status=$? if (( $status )) then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[alert]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -773,7 +762,7 @@ $CLI_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } @@ -782,9 +771,9 @@ $CLI_LINE" { local state=$(/boot/dietpi/dietpi-vpn status 2>&1) case $state in - Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}";; - Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}";; - *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}";; + Connected*) state="${aCOLOUR[weak]}$state${COLOUR_RESET}";; + Disconnected*) state="${aCOLOUR[alert]}$state${COLOUR_RESET}";; + *) state="${aCOLOUR[highlight]}$state${COLOUR_RESET}";; esac echo "$state" } @@ -819,9 +808,9 @@ $CLI_LINE" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[disk_rootfs]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_rootfs]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_rootfs]} )) && echo -e "$CLI_BULLET ${aCOLOUR[strong]}${aDESCRIPTION[disk_rootfs]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[disk_userdata]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_userdata]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_userdata]} )) && echo -e "$CLI_BULLET ${aCOLOUR[strong]}${aDESCRIPTION[disk_userdata]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather (( ${aENABLED[weather]} )) && Print_Item_State "${aDESCRIPTION[weather]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status @@ -859,7 +848,7 @@ $CLI_LINE" (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors were migrated from legacy numeric indices during this run - (( $DIETPI_BANNER_MIGRATED_DURING_RUN || $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } Print_Banner() @@ -878,20 +867,20 @@ $CLI_LINE" Menu_Main() { G_WHIP_CHECKLIST_ARRAY=() - for key in "${MENU_ITEMS[@]}" + for k in "${MENU_ITEMS[@]}" do local state='off' - (( ${aENABLED[$key]:=0} == 1 )) && state='on' - G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") + (( ${aENABLED[$k]:=0} == 1 )) && state='on' + G_WHIP_CHECKLIST_ARRAY+=("$k" "${aDESCRIPTION[$k]}" "$state") done G_WHIP_CHECKLIST_ENUM=1 # set the menu to use integer indices for the display G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 G_WHIP_CHECKLIST_ENUM=0 - for key in "${MENU_ITEMS[@]}" + for k in "${MENU_ITEMS[@]}" do - aENABLED[$key]=0 + aENABLED[$k]=0 done for selection in $G_WHIP_RETURNED_VALUE @@ -902,7 +891,7 @@ $CLI_LINE" then # ignore check for single/double quote expansion issue; unassigned var is referenced; # shellcheck disable=SC2016,SC2154 - example_command='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + example_command='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' # Manage custom commands list while : @@ -941,7 +930,7 @@ $CLI_LINE" done # If no commands are configured, disable the custom commands option - (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && aENABLED[custom_commands]=0 + (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 elif [[ $selection == 'word_wrap' ]] then @@ -1010,9 +999,9 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Before saving, create a backup of the existing prefs if migrating. # FP_SAVEFILE gets truncated when calling Save() below, so we need to backup first. - if [[ -f $FP_SAVEFILE && ( $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 || $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN == 1 ) ]] + if [[ -f $FP_SAVEFILE && $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]] then - backup="$FP_SAVEFILE.bak-$(date -Iseconds)" + local backup="$FP_SAVEFILE.bak-$(date -Iseconds)" cp -a "$FP_SAVEFILE" "$backup" fi From c3e0863a5ccbd3721acc3075b7847a2d0306d547 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 17:42:12 +0200 Subject: [PATCH 26/69] dietpi-banner: some more alignment dietpi-globals: revert changes to G_GET_WAN_IP until we found a sane solution for namespaces with sudo, and since the banner does not currently use it. Align coding in G_TRUNCATE_MID --- dietpi/func/dietpi-banner | 60 +++++++++++++++++++++----------------- dietpi/func/dietpi-globals | 55 ++++++++++++++++------------------ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index ae4ab8fed0..dcf1975c17 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -133,8 +133,9 @@ { # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form local k legacy_indexed_enabled_keys=( - device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd - vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata + weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt + ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status ) for k in "${!aENABLED[@]}" do @@ -356,7 +357,7 @@ $CLI_LINE" if [[ $EUID == 0 ]] then # Running as root - stat=$(Get_Cert_Status_Raw 2>/dev/null) + stat=$(Get_Cert_Status_Raw 2> /dev/null) code=$? else # Running as non-root: Fail silently without NOPASSWD to avoid password prompt @@ -454,17 +455,17 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - if command -v fail2ban-client >/dev/null 2>&1 + if command -v fail2ban-client > /dev/null then if [[ $EUID == 0 ]] then - f2b_output="$(fail2ban-client banned 2>/dev/null)" + f2b_output="$(fail2ban-client banned 2> /dev/null)" else - f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" + f2b_output="$(sudo -n fail2ban-client banned 2> /dev/null || return 2)" [[ $? == 2 ]] && return 2 fi - if ! command -v sort >/dev/null 2>&1 + if ! command -v sort > /dev/null 2>&1 then sort() { cat; } # fallback if sort is not available, simply passes input through fi @@ -517,10 +518,10 @@ $CLI_LINE" # Print public IP address and location info # - Optional arguments: - # -t : Set timeout in seconds, supports floats, default: 3 + # -t : Set timeout in seconds, supports floats, default: 3 # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific - # network namespace, default: empty (no namespace = the main default namespace) - BANNER_GET_WAN_IP() + # network namespace, default: empty (no namespace = the main default namespace) + Get_WAN_IP() { # Defaults local timeout=3 @@ -569,7 +570,7 @@ $CLI_LINE" fi # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | mawk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2> /dev/null | mawk -F': ' '{print $2}') for IFACE in "${observed_ifaces[@]}" do @@ -591,17 +592,17 @@ $CLI_LINE" local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr # Get RX data - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null) + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2> /dev/null) [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get TX data - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null) + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2> /dev/null) [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" ip_addr=' - No IP - ' - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | mawk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") - [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2> /dev/null | mawk '{print $4}' | head -1 | grep -E "$IP4_re_cidr") + [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue printf '%s %s%-20s %s%s TX= %11s RX= %11s%s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" "$COLOUR_RESET" @@ -617,14 +618,14 @@ $CLI_LINE" raw_ns=${ns:-default} display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) - IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) + IP=$(Get_WAN_IP -t 1 -n "$ns" 2>&1) if [[ $IP =~ $IP4_re ]] then # Valid IP detected, keep as-is and print with namespace - # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. - # IP="${BASH_REMATCH[0]}" + # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + #IP=${BASH_REMATCH[0]} printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" @@ -804,7 +805,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(BANNER_GET_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(Get_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) @@ -896,12 +897,17 @@ $CLI_LINE" # Manage custom commands list while : do - G_WHIP_MENU_ARRAY=( 'Add' 'Add a new command' 'Edit' 'Edit an existing command' 'Remove' 'Remove a command' 'Done' 'Finish custom commands configuration' ) + G_WHIP_MENU_ARRAY=( + 'Add' 'Add a new command' + 'Edit' 'Edit an existing command' + 'Remove' 'Remove a command' + 'Done' 'Finish custom commands configuration' + ) G_WHIP_DEFAULT_ITEM='Done' - G_WHIP_MENU 'Manage custom banner commands.\n\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit :' || break + G_WHIP_MENU 'Manage custom banner commands.\n\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit:' || break case "$G_WHIP_RETURNED_VALUE" in Add) - G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue + G_WHIP_INPUTBOX "Enter new command, for example:\n$example_command" || continue aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) @@ -911,7 +917,7 @@ $CLI_LINE" G_WHIP_MENU 'Select command to edit:' || continue sel=$G_WHIP_RETURNED_VALUE G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} - G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue + G_WHIP_INPUTBOX "Edit command, for example:\n$example_command" || continue aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) @@ -964,24 +970,24 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" G_WHIP_MENU "Manage disk space match patterns. $pattern_hint" || break case "$G_WHIP_RETURNED_VALUE" in Add) - G_WHIP_INPUTBOX "Enter new pattern :\n$pattern_hint" || continue + G_WHIP_INPUTBOX "Enter new pattern:\n$pattern_hint" || continue aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to edit'; continue; } G_WHIP_MENU_ARRAY=() for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done - G_WHIP_MENU 'Select pattern to edit :' || continue + G_WHIP_MENU 'Select pattern to edit:' || continue sel=$G_WHIP_RETURNED_VALUE G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} - G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue + G_WHIP_INPUTBOX "Edit pattern:\n$pattern_hint" || continue aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to remove'; continue; } G_WHIP_MENU_ARRAY=() for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done - G_WHIP_MENU 'Select pattern to remove :' || continue + G_WHIP_MENU 'Select pattern to remove:' || continue sel=$G_WHIP_RETURNED_VALUE unset 'aDISK_SPACE_PATTERNS[$sel]' # reindex array diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index e85fb08620..683853155e 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -176,25 +176,28 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". # - E.g. # For example: - # `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" - # `G_TRUNCATE_MID "Long text" 5` returns "L...t" - # `G_TRUNCATE_MID "Text" 3` returns "Tex" - G_TRUNCATE_MID() { - local s="$1"; local max="$2" - local len=${#s} - if (( len <= max )); then - printf '%s' "$s" - return - fi - if (( max <= 3 )); then - printf '%.*s' "$max" "$s" - return - fi - local keep=$((max - 3)) - local pre=$(((keep + 1) / 2)) - local suf=$((keep / 2)) - local start=$((len - suf)) - printf '%s...%s' "${s:0:pre}" "${s:start}" + # - `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" + # - `G_TRUNCATE_MID "Long text" 5` returns "L...t" + # - `G_TRUNCATE_MID "Text" 3` returns "Tex" + G_TRUNCATE_MID() + { + local s=$1 max=$2 + local len=${#s} + if (( $max < $len )) + then + echo "$s" + return 0 + + elif (( $max < 4 )) + then + echo "${s:0:$max}" + return 0 + fi + local keep=$(( $max - 3 )) + local pre=$(( ( $keep + 1 ) / 2 )) + local suf=$(( $keep / 2 )) + local start=$(( $len - $suf )) + echo "${s:0:pre}...${s:start}" } # DietPi-Notify @@ -1541,29 +1544,21 @@ Press any key to continue...' # Print public IP address and location info # - Optional arguments: # -t : Set timeout in seconds, supports floats, default: 3 - # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific - # network namespace, default: empty (no namespace = the main default namespace) G_GET_WAN_IP() { # Defaults local timeout=3 - local ns="" # Inputs while (( $# )) do + # shellcheck disable=SC2015 case $1 in - '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; - '-n') shift; ns="${1:-}";; + '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done - - if [[ -n "$ns" ]]; then - ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - else - curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - fi + curl -sSfLm "$timeout" 'https://dietpi.com/geoip' } # $1 = directory to test permissions support From 02f00ccb8e3bd7de44020037eb89ec6492a74712 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 18:47:17 +0200 Subject: [PATCH 27/69] dietpi-globals: simplify G_WHIP_CHECKLIST_ENUM handling --- dietpi/func/dietpi-banner | 1 - dietpi/func/dietpi-globals | 50 +++++++++++++++----------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index dcf1975c17..4aff02b989 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -877,7 +877,6 @@ $CLI_LINE" G_WHIP_CHECKLIST_ENUM=1 # set the menu to use integer indices for the display G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 - G_WHIP_CHECKLIST_ENUM=0 for k in "${MENU_ITEMS[@]}" do diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 683853155e..781eeddfaf 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -489,12 +489,12 @@ $grey───────────────────────── # - G_WHIP_NOCANCEL=1 | Optional, hide the cancel button on inputbox, menu and checkbox dialogues # - G_WHIP_MENU_ARRAY | Required for G_WHIP_MENU to set available menu entries, 2 array indices per line: ('item' 'description') # - G_WHIP_CHECKLIST_ARRAY | Required for G_WHIP_CHECKLIST set available checklist options, 3 array indices per line: ('item' 'description' 'on'/'off') - # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags + # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags # Output: # - G_WHIP_RETURNED_VALUE | Returned value from inputbox/menu/checklist based whiptail items # G_WHIP_DESTROY | Clear vars after run of whiptail - G_WHIP_DESTROY(){ unset -v G_WHIP_DEFAULT_ITEM G_WHIP_SIZE_X_MAX G_WHIP_BUTTON_OK_TEXT G_WHIP_BUTTON_CANCEL_TEXT G_WHIP_NOCANCEL G_WHIP_MENU_ARRAY G_WHIP_CHECKLIST_ARRAY G_WHIP_INPUTBOX_REGEX G_WHIP_INPUTBOX_REGEX_TEXT; } + G_WHIP_DESTROY(){ unset -v G_WHIP_DEFAULT_ITEM G_WHIP_SIZE_X_MAX G_WHIP_BUTTON_OK_TEXT G_WHIP_BUTTON_CANCEL_TEXT G_WHIP_NOCANCEL G_WHIP_MENU_ARRAY G_WHIP_CHECKLIST_ARRAY G_WHIP_INPUTBOX_REGEX G_WHIP_INPUTBOX_REGEX_TEXT G_WHIP_CHECKLIST_ENUM; } # Run once, to be failsafe in case any exported/environment variables are left from originating shell G_WHIP_DESTROY @@ -835,7 +835,7 @@ $grey───────────────────────── # G_WHIP_CHECKLIST "message" # - Prompt user to select multiple options from G_WHIP_CHECKLIST_ARRAY and save choice to G_WHIP_RETURNED_VALUE # - Exit code: 0=selection done, else=user cancelled or noninteractive - # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags + # - G_WHIP_CHECKLIST_ENUM=1: Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags G_WHIP_CHECKLIST() { local result=1 @@ -847,26 +847,17 @@ $grey───────────────────────── [[ $G_WHIP_NOCANCEL == 1 ]] && NOCANCEL=('--nocancel') G_WHIP_BUTTON_OK_TEXT=${G_WHIP_BUTTON_OK_TEXT:-Confirm} - # Places to save original checklist and default - local orig_checklist=() original_default_item="$G_WHIP_DEFAULT_ITEM" - - #### - # If G_WHIP_CHECKLIST_ENUM=1, the checklist will be enumerated with numeric tags for whiptail, - # while preserving the original tags for mapping backwards after the checklist selection. - if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#G_WHIP_CHECKLIST_ARRAY[@]} -gt 0 ]] + # If G_WHIP_CHECKLIST_ENUM=1 is set, map and replace original tags and default with numeric ones + if (( $G_WHIP_CHECKLIST_ENUM )) then - # save the original checklist - orig_checklist=("${G_WHIP_CHECKLIST_ARRAY[@]}") - - # Build numeric-tagged checklist for whiptail while preserving original tags - local -a __tmp=() __idx=0 __map=() - for ((i=0;i<${#orig_checklist[@]};i+=3)); do - __map[$__idx]="${orig_checklist[$i]}" - __tmp+=("$__idx" "${orig_checklist[$i+1]}" "${orig_checklist[$i+2]}") - [[ -n "$original_default_item" && "$original_default_item" == "${orig_checklist[$i]}" ]] && G_WHIP_DEFAULT_ITEM=$__idx - ((__idx++)) + local map=() idx=0 + for ((i=0;i<${#G_WHIP_CHECKLIST_ARRAY[@]};i+=3)) + do + [[ $G_WHIP_DEFAULT_ITEM == "${G_WHIP_CHECKLIST_ARRAY[$i]}" ]] && G_WHIP_DEFAULT_ITEM=$idx + map[$idx]=${G_WHIP_CHECKLIST_ARRAY[$i]} + G_WHIP_CHECKLIST_ARRAY[$i]=$idx + ((idx++)) done - G_WHIP_CHECKLIST_ARRAY=("${__tmp[@]}") fi G_WHIP_INIT 3 @@ -874,18 +865,15 @@ $grey───────────────────────── G_WHIP_RETURNED_VALUE=$(whiptail ${G_PROGRAM_NAME:+--title "$G_PROGRAM_NAME"} --backtitle "$WHIP_BACKTITLE | Use spacebar to toggle selection" --checklist "$WHIP_MESSAGE" --separate-output --ok-button "$G_WHIP_BUTTON_OK_TEXT" --cancel-button "$G_WHIP_BUTTON_CANCEL_TEXT" "${NOCANCEL[@]}" --default-item "$G_WHIP_DEFAULT_ITEM" $WHIP_SCROLLTEXT "$WHIP_SIZE_Y" "$WHIP_SIZE_X" "$WHIP_SIZE_Z" -- "${G_WHIP_CHECKLIST_ARRAY[@]}" 3>&1 1>&2 2>&3-) result=$? - # If enumeration was used, map numeric selections back to original tags - if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#orig_checklist[@]} -gt 0 && -n "$G_WHIP_RETURNED_VALUE" ]] + # If G_WHIP_CHECKLIST_ENUM=1 is set, replace numeric selections back to original tags + if (( $G_WHIP_CHECKLIST_ENUM )) then - local sel mapped=() - for sel in $G_WHIP_RETURNED_VALUE; do - mapped+=("${__map[$sel]}") + local mapped=() + for idx in $G_WHIP_RETURNED_VALUE + do + mapped+=("${map[$idx]}") done - G_WHIP_RETURNED_VALUE="${mapped[*]}" - # restore originals - G_WHIP_CHECKLIST_ARRAY=("${orig_checklist[@]}") - G_WHIP_DEFAULT_ITEM="$original_default_item" - unset -v __map __tmp __idx sel mapped + G_WHIP_RETURNED_VALUE=${mapped[*]} fi G_WHIP_RETURNED_VALUE=$(echo -e "$G_WHIP_RETURNED_VALUE" | tr '\n' ' ') From da439898d1795ff331bbcaf01a50344b24306b21 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 7 Jul 2026 22:55:53 -0500 Subject: [PATCH 28/69] fixed truncate length comparison and improved the output for shorter max lengths --- dietpi/func/dietpi-globals | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 781eeddfaf..1e29695d06 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -183,12 +183,12 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue { local s=$1 max=$2 local len=${#s} - if (( $max < $len )) + if (( $max > $len )) then echo "$s" return 0 - elif (( $max < 4 )) + elif (( $max < 7 )) then echo "${s:0:$max}" return 0 From 03ef77d04315766cf2998a1119d3846b1078b60d Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 7 Jul 2026 22:57:18 -0500 Subject: [PATCH 29/69] cleaned up netns data and reduced sudo usage --- dietpi/func/dietpi-banner | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 4aff02b989..8a280c3ea0 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -590,13 +590,35 @@ $CLI_LINE" esac local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr + local rx_path="/sys/class/net/$ifname/statistics/rx_bytes" + local tx_path="/sys/class/net/$ifname/statistics/tx_bytes" - # Get RX data - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2> /dev/null) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + if (( EUID != 0 )) && [ -z "$netns" ]; + then + # If we are not root and no netns is specified, check if we can read + # the RX/TX statistics files directly, otherwise fall back to sudo + if [ -r "$rx_path" ]; + then + rx=$(cat "$rx_path" 2> /dev/null) + else + # If we are not root and no netns is specified, use sudo + rx=$(sudo -n cat "$rx_path" 2> /dev/null) + fi + + if [ -r "$tx_path" ]; + then + tx=$(cat "$tx_path" 2> /dev/null) + else + tx=$(sudo -n cat "$tx_path" 2> /dev/null) + fi + else + # If we are root or a netns is specified, use the prefix to read the RX/TX statistics files + rx=$("${ns_cmd_prefix[@]}" cat "$rx_path" 2> /dev/null) + tx=$("${ns_cmd_prefix[@]}" cat "$tx_path" 2> /dev/null) + fi - # Get TX data - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2> /dev/null) + # Process the RX/TX values to convert to GiB if they are numeric, otherwise leave as 'N/A' + [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" From faf7c9744e5297238f268d85861d567f8a355672 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 18:10:40 +0200 Subject: [PATCH 30/69] dietpi-banner: some fixes and changes * restore dash-compatibility of Get_Cert_Status_Raw * cleanup Get_Cert_Status: `sudo -n` returns the exit code "1" if sudo permissions are missing, the "no cert found" error code would hence need to swap. However, there is already the exit code 0 handling based on the "No certificate found" output, so make us of that instead. If Get_Cert_Status_Raw runs at all, exit code 0 is now always returned, hence forge banner line based on its output. In case of another exit code, it must have come from a sudo failure, hence print the error about NOPASSWD sudo. * Move `Print_*` functions below `Get_*` functions, and rename `Get_*` functions which are supposed to print the formatted output to console directly to `Print_*` * Remove trailing COLOUR_RESET where not required * Simplify Get_Fail2Ban_Status: process `fail2ban-client` output with a single awk to avoid the large amount of external command calls, and print it directly. * create variables as local ones where appropriate * Simplify Print_Network_Usage: The `/sys/class/net/*/statistics` is always world-readable (unless the kernel is manipulated). Skip related checks, and always read it directly if no namespace is used, using the cheaper builtin `read` instead of external `cat` call. Tailore Get_WAN_IP. Parse simpler `ip -br` output instead of `ip -o`. Print curl errors as is. They should be short with `-sSf`, and leaving them untouched helps with debugging. --- dietpi/func/dietpi-banner | 348 +++++++++++++++---------------------- dietpi/func/dietpi-globals | 2 +- 2 files changed, 145 insertions(+), 205 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 8a280c3ea0..fb68e048d5 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -184,7 +184,6 @@ # IP address detection (IPv4) IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' - IP4_re_cidr='([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}' # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" @@ -339,25 +338,23 @@ $CLI_LINE" Get_Cert_Status_Raw() { # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. - # also is `dash` compatible + # Keep it dash-compatible local i for i in /etc/letsencrypt/live/*/cert.pem do - # disable shellcheck for 'require-double-brackets' so it is `dash` compatible # shellcheck disable=SC2292 - [[ -f $i ]] || continue + [ -f "$i" ] || continue openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' return 0 done - return 1 + echo 'No certificate found' } - stat= - code=0 + local stat code if [[ $EUID == 0 ]] then # Running as root - stat=$(Get_Cert_Status_Raw 2> /dev/null) + stat=$(Get_Cert_Status_Raw 2>&1) code=$? else # Running as non-root: Fail silently without NOPASSWD to avoid password prompt @@ -367,88 +364,32 @@ $CLI_LINE" case $code in 0) - # stat is valid + # Get_Cert_Status_Raw executed, process output case $stat in - Valid*) stat="${aCOLOUR[weak]}$stat${COLOUR_RESET}";; - No\ certificate*) stat="${aCOLOUR[alert]}$stat${COLOUR_RESET}";; - *) stat="${aCOLOUR[highlight]}$stat${COLOUR_RESET}";; + 'Valid until'*) stat="${aCOLOUR[weak]}$stat";; # valid cert, print line with weak color + 'No certificate found') stat="${aCOLOUR[highlight]}$stat";; # highlight if there is no cert + *) stat="${aCOLOUR[alert]}$stat";; # alert if cert expired or there was another error esac ;; - 1) stat="${aCOLOUR[alert]}No certificate found${COLOUR_RESET}";; - 2) stat="${aCOLOUR[highlight]}NOPASSWD sudo required${COLOUR_RESET}";; - *) stat="${aCOLOUR[highlight]}Unable to obtain cert status${COLOUR_RESET}";; + *) + # sudo failed, print meaningful error message + stat="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain cert status" + ;; esac echo "$stat" } - Print_Credits() - { - echo -e " ${aCOLOUR[weak]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" - - [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info - - echo -e " Patreon Legends : ADSB.im, Oliver C. Radke - Website : https://dietpi.com/ | https://x.com/DietPi_ | Bsky: @dietpi.com - Contribute : https://dietpi.com/contribute.html - Web Hosting by : https://login-online.com/$COLOUR_RESET\n" - } - - Print_Updates() - { - # DietPi update available? - if [[ $AVAILABLE_UPDATE ]] - then - echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" - - # New DietPi live patches available? - elif (( $LIVE_PATCHES )) - then - echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" - - # APT updates available? - elif (( $PACKAGE_COUNT )) - then - echo -e " ${aCOLOUR[strong]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" - - # Reboot required to finalise kernel upgrade? - elif (( $REBOOT_REQUIRED )) - then - echo -e " ${aCOLOUR[strong]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[alert]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" - fi - } - - Print_Useful_Commands() - { - echo -e " ${aCOLOUR[strong]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[strong]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[strong]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation - ${aCOLOUR[strong]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor - ${aCOLOUR[strong]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" - } - - Print_Item_State() - { - # Print a single line item with green bullet, description "$1", green separator and state "$2" - # state should contain color and format codes as needed - local subtitle=$1 - local state=$2 - local width=${3:-} - local color=${aCOLOUR[strong]} - printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" - } - Get_Systemd_Status() { # Print systemd overall status based on "systemctl --failed" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] then - state="${aCOLOUR[weak]}No Services Failed${COLOUR_RESET}" + echo "${aCOLOUR[weak]}No services failed" else - state="${aCOLOUR[alert]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + echo "${aCOLOUR[alert]}$FAILED_CNT service(s) failed" fi - echo "$state" } Get_Fail2Ban_Status() @@ -459,28 +400,10 @@ $CLI_LINE" then if [[ $EUID == 0 ]] then - f2b_output="$(fail2ban-client banned 2> /dev/null)" + fail2ban-client banned 2> /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /dev/null 2>&1 - then - sort() { cat; } # fallback if sort is not available, simply passes input through - fi - - # get banned IP address count - IP_addrs="$(echo "$f2b_output" | grep -o '\[[^]]*\]' | grep -Eo "$IP4_re" | sort -u)" - if [[ ! "$IP_addrs" ]] - then - IP_addrs= - count_banned_ips=0 - else - count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" - fi - echo "$count_banned_ips" - return 0 else # fail2ban-client command is not available @@ -488,73 +411,53 @@ $CLI_LINE" fi } + local count_banned_ips count_banned_ips=$(Get_Fail2Ban_Status_Raw) code=$? case $code in 0) - if [[ $count_banned_ips -eq 0 ]] + if (( $count_banned_ips == 0 )) then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[highlight]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" + state="${aCOLOUR[highlight]}No IPs banned, check Fail2Ban state" + + elif (( $count_banned_ips > 20 )) + then + # high number of bans + state="${aCOLOUR[alert]}$count_banned_ips IP banned" else - if [[ $count_banned_ips -gt 20 ]] - then - # high number of bans - state="${aCOLOUR[alert]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" - else - # low number of bans - state="${aCOLOUR[weak]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" - fi + # low number of bans + state="${aCOLOUR[weak]}$count_banned_ips IP(s) banned" fi ;; - 1) state="${aCOLOUR[alert]}fail2ban-client not available${COLOUR_RESET}";; - 2) state="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}";; - *) state="${aCOLOUR[alert]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}";; + 1) state="${aCOLOUR[alert]}fail2ban-client not available";; + 2) state="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain Fail2Ban status";; + *) state="${aCOLOUR[alert]}Unknown state, code: $code count: $count_banned_ips";; esac echo "$state" } - # Print public IP address and location info - # - Optional arguments: - # -t : Set timeout in seconds, supports floats, default: 3 - # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific - # network namespace, default: empty (no namespace = the main default namespace) - Get_WAN_IP() + Print_Network_Usage() { - # Defaults - local timeout=3 - local netns - # Inputs - while (( $# )) - do - case $1 in - '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; - '-n') shift; netns="${1:-}";; - *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; - esac - shift - done - - local ns_cmd_prefix=() - if [[ $netns ]] - then - # add sudo if not root - (( $EUID )) && ns_cmd_prefix+=(sudo -n) - # add netns prefix - ns_cmd_prefix+=(ip netns exec "$netns") - fi - "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - } - - Get_Network_Usage() - { - # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) - local err_re='timed out|timeout|could not resolve|not known|unknown' + # Get WAN IP for namespace $1 (default if empty) + Get_WAN_IP() + { + local ns_cmd_prefix=() + if [[ $1 ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) + # add netns prefix + ns_cmd_prefix+=(ip netns exec "$1") + fi + "${ns_cmd_prefix[@]}" curl -sSfLm 1 'https://dietpi.com/geoip' + } - DATA_USAGE() + # Print usage stats for namespace $1 (default if empty) + Print_Data_Usage() { local netns=$1 @@ -570,7 +473,7 @@ $CLI_LINE" fi # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2> /dev/null | mawk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -br link 2> /dev/null | mawk '{print $1}') for IFACE in "${observed_ifaces[@]}" do @@ -586,52 +489,38 @@ $CLI_LINE" wlan*|wl*) :;; # vpn/tunnel/virtual tun*|wg*|vpn*) :;; - *) continue ;; + *) continue;; esac - local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr + local RX='N/A' TX='N/A' rx tx ip_addr=' - No IP - ' ip_addr_cidr local rx_path="/sys/class/net/$ifname/statistics/rx_bytes" local tx_path="/sys/class/net/$ifname/statistics/tx_bytes" - if (( EUID != 0 )) && [ -z "$netns" ]; + if [[ $netns ]]; then - # If we are not root and no netns is specified, check if we can read - # the RX/TX statistics files directly, otherwise fall back to sudo - if [ -r "$rx_path" ]; - then - rx=$(cat "$rx_path" 2> /dev/null) - else - # If we are not root and no netns is specified, use sudo - rx=$(sudo -n cat "$rx_path" 2> /dev/null) - fi - - if [ -r "$tx_path" ]; - then - tx=$(cat "$tx_path" 2> /dev/null) - else - tx=$(sudo -n cat "$tx_path" 2> /dev/null) - fi - else - # If we are root or a netns is specified, use the prefix to read the RX/TX statistics files + # If a netns is specified, use the prefix to read the RX/TX statistics files. rx=$("${ns_cmd_prefix[@]}" cat "$rx_path" 2> /dev/null) tx=$("${ns_cmd_prefix[@]}" cat "$tx_path" 2> /dev/null) + else + # Else, read the RX/TX statistics files directly. + read -r rx < "$rx_path" + read -r tx < "$tx_path" fi # Process the RX/TX values to convert to GiB if they are numeric, otherwise leave as 'N/A' [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=' - No IP - ' - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2> /dev/null | mawk '{print $4}' | head -1 | grep -E "$IP4_re_cidr") + # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9]{1,2}$" '$3 ~ re {print $3;exit}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - printf '%s %s%-20s %s%s TX= %11s RX= %11s%s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" "$COLOUR_RESET" + echo -e "$CLI_BULLET ${aCOLOUR[weak]}$ip_addr ($ifname) $CLI_SEPARATOR ${aCOLOUR[weak]}TX=$TX RX=$RX" done } - mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 { print $1 }') + mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 {print $1}') # Prepend an empty entry so the default (root) namespace can be handled in the loop namespaces=('' "${namespaces[@]}") @@ -640,51 +529,32 @@ $CLI_LINE" raw_ns=${ns:-default} display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) - IP=$(Get_WAN_IP -t 1 -n "$ns" 2>&1) + IP=$(Get_WAN_IP "$ns" 2>&1) - if [[ $IP =~ $IP4_re ]] + if [[ $IP =~ ^$IP4_re ]] then # Valid IP detected, keep as-is and print with namespace # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. #IP=${BASH_REMATCH[0]} - printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" + echo -e "${aCOLOUR[alt]} $IP [$display_ns]" elif [[ ! $IP ]] then - # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. - printf '%s %s %s %s%s%s\n' "${aCOLOUR[alert]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$display_ns" "$COLOUR_RESET" + # If the IP is empty, it likely means the namespace is disconnected or has no Internet access. Print status message. + echo -e "${aCOLOUR[alert]} Disconnected $CLI_SEPARATOR ${aCOLOUR[strong]}$display_ns" else - # Treat known curl/network error messages as timeout/DNS failures - [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' - - # If the IP contains an unknown or error message, print it with the namespace - printf ' %s%s [%s]%s\n' "${aCOLOUR[highlight]}" "$IP" "$display_ns" "$COLOUR_RESET" + # Print curl errors as is + echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" fi - DATA_USAGE "$ns" + Print_Data_Usage "$ns" done } - Print_Custom_Commands() - { - for i in "${!aCUSTOM_COMMANDS[@]}" - do - [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue - output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) - status=$? - if (( $status )) - then - printf '%b\n' "${aCOLOUR[alert]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" - else - printf '%b\n' "$output" - fi - done - } - - Get_Disk_Usage() + Print_Disk_Usage() { # If no arguments are provided, default to '"/" "/mnt/*"'. (( $# )) && aDISK_SPACE_PATTERNS=("$@") @@ -801,6 +671,76 @@ $CLI_LINE" echo "$state" } + Print_Custom_Commands() + { + local output status + for i in "${!aCUSTOM_COMMANDS[@]}" + do + [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue + output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) + status=$? + if (( $status )) + then + echo -e "${aCOLOUR[alert]}Custom command $i failed ($status): $output" + else + echo -e "$output" + fi + done + } + + Print_Credits() + { + echo -e " ${aCOLOUR[weak]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + + [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info + + echo -e " Patreon Legends : ADSB.im, Oliver C. Radke + Website : https://dietpi.com/ | https://x.com/DietPi_ | Bsky: @dietpi.com + Contribute : https://dietpi.com/contribute.html + Web Hosting by : https://login-online.com/$COLOUR_RESET\n" + } + + Print_Updates() + { + # DietPi update available? + if [[ $AVAILABLE_UPDATE ]] + then + echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + + # New DietPi live patches available? + elif (( $LIVE_PATCHES )) + then + echo -e " ${aCOLOUR[strong]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + + # APT updates available? + elif (( $PACKAGE_COUNT )) + then + echo -e " ${aCOLOUR[strong]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[highlight]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + + # Reboot required to finalise kernel upgrade? + elif (( $REBOOT_REQUIRED )) + then + echo -e " ${aCOLOUR[strong]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[alert]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + fi + } + + Print_Useful_Commands() + { + echo -e " ${aCOLOUR[strong]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[strong]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[strong]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation + ${aCOLOUR[strong]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor + ${aCOLOUR[strong]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" + } + + Print_Item_State() + { + # Print a single line item with green bullet, description "$1", green separator and state "$2" + # state should contain color and format codes as needed + local subtitle=$1 state=$2 width=$3 + printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" + } + Print_Banner_raw() { Print_Header @@ -827,7 +767,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(Get_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(G_GET_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) @@ -844,13 +784,13 @@ $CLI_LINE" (( ${aENABLED[fail2ban_status]} )) && Print_Item_State "${aDESCRIPTION[fail2ban_status]}" "$(Get_Fail2Ban_Status 2>&1)" # Network usage by namespace - # %b required so that percent symbols are not interpreted as format specifiers by printf - (( ${aENABLED[network_usage]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" - # Disk Usage - (( ${aENABLED[disk_usage]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1)" + (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 + + # Disk usage + (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1 # Custom commands - (( ${aENABLED[custom_commands]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands # Separator line (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 1e29695d06..f49becd7eb 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -183,7 +183,7 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue { local s=$1 max=$2 local len=${#s} - if (( $max > $len )) + if (( $max >= $len )) then echo "$s" return 0 From eb98749f96751cbe76cc62546fc160ace4cf252a Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 19:01:05 +0200 Subject: [PATCH 31/69] dietpi-banner: shorten some option keys --- dietpi/func/dietpi-banner | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index fb68e048d5..83790f0680 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -55,11 +55,11 @@ [disk_userdata]='Disk usage (userdata)' [weather]='Weather (wttr.in)' [custom_commands]='Custom commands' - [useful_commands]='Display DietPi useful commands?' + [dietpi_commands]='Display useful DietPi commands?' [motd]='MOTD' [vpn_status]='VPN status' [large_hostname]='Large hostname' - [print_credits]='Print credits' + [credits]='Print credits' [letsencrypt]='Let'\''s Encrypt cert status' [ram_usage]='RAM usage' [load_average]='Load average' @@ -67,14 +67,14 @@ [kernel]='Kernel' [network_usage]='Network Usage' [disk_usage]='Disk Usage' - [systemd_status]='Systemd Status' - [fail2ban_status]='Fail2Ban Status' + [systemd]='Systemd Status' + [fail2ban]='Fail2Ban Status' ) # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname - lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status - network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap + lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd fail2ban + network_usage disk_usage custom_commands dietpi_commands motd credits word_wrap ) # Initialize aENABLED from aDESCRIPTION keys @@ -88,9 +88,9 @@ aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 - aENABLED[useful_commands]=1 + aENABLED[dietpi_commands]=1 aENABLED[motd]=1 - aENABLED[print_credits]=1 + aENABLED[credits]=1 # Enable CPU temp by default except in VMs (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 @@ -134,8 +134,8 @@ # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form local k legacy_indexed_enabled_keys=( device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata - weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt - ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status + weather custom_commands dietpi_commands motd vpn_status large_hostname credits letsencrypt + ram_usage load_average word_wrap kernel network_usage disk_usage systemd fail2ban ) for k in "${!aENABLED[@]}" do @@ -779,36 +779,32 @@ $CLI_LINE" # Let's Encrypt cert status (( ${aENABLED[letsencrypt]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" # Systemd status - (( ${aENABLED[systemd_status]} )) && Print_Item_State "${aDESCRIPTION[systemd_status]}" "$(Get_Systemd_Status 2>&1)" + (( ${aENABLED[systemd]} )) && Print_Item_State "${aDESCRIPTION[systemd]}" "$(Get_Systemd_Status 2>&1)" # Fail2Ban status - (( ${aENABLED[fail2ban_status]} )) && Print_Item_State "${aDESCRIPTION[fail2ban_status]}" "$(Get_Fail2Ban_Status 2>&1)" - + (( ${aENABLED[fail2ban]} )) && Print_Item_State "${aDESCRIPTION[fail2ban]}" "$(Get_Fail2Ban_Status 2>&1)" # Network usage by namespace (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 - # Disk usage (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1 - # Custom commands (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands # Separator line - (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" + (( ${aENABLED[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[credits]} )) && echo -e "$CLI_LINE" # MOTD if (( ${aENABLED[motd]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" - # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[motd]}" "$motd" fi echo -e "$CLI_LINE\n" - (( ${aENABLED[print_credits]} )) && Print_Credits + (( ${aENABLED[credits]} )) && Print_Credits Print_Updates - (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands + (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors were migrated from legacy numeric indices during this run (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" From ce76422da53ed3db86a8249208f8ac20c80a5c00 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 19:39:37 +0200 Subject: [PATCH 32/69] dietpi-banner: fix disk usage and align new item descriptions --- dietpi/func/dietpi-banner | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 83790f0680..e6416ac209 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -65,10 +65,10 @@ [load_average]='Load average' [word_wrap]='Word-wrap lines on small screens' [kernel]='Kernel' - [network_usage]='Network Usage' - [disk_usage]='Disk Usage' - [systemd]='Systemd Status' - [fail2ban]='Fail2Ban Status' + [network_usage]='Network usage' + [disk_usage]='Disk usage' + [systemd]='systemd status' + [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( @@ -561,8 +561,7 @@ $CLI_LINE" # TODO: is there a more efficient reader than df (stat?) # Use df's output to parse numeric fields (kB units) for mounts matching the supplied patterns - length=0 - results=() + local length=0 results=() size_kb used_kb avail_kb mnt_target name matched basename while read -r size_kb used_kb avail_kb mnt_target do matched=0 @@ -598,15 +597,15 @@ $CLI_LINE" (( $len > $length )) && length=$len results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") fi - done < <(df -k --output=size,used,avail,target | tail -n +2) + done < <(df -k --output=size,used,avail,target | tail +2) - # Print results with aligned columns, converting kB to GB and calculating percentage used + # Print results with aligned columns, converting KiB to GiB and calculating percentage used for entry in "${results[@]}" do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" local values_count=0 - # Do all math in kB, convert to GB for display and round to 1 decimal + # Do all math in KiB, convert to GiB for display and round to 1 decimal if [[ $size_kb =~ ^[0-9]+$ ]] then ((values_count++)) @@ -655,9 +654,8 @@ $CLI_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%b %b%-${length}s %b %b%6b %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" done - } Get_VPN_Status() From 7d7ce5ad1e689b095a1d6a12fef50015b605fc73 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 23:49:39 +0200 Subject: [PATCH 33/69] dietpi-banner: little cleanup * remove redundant carriage return: Print_Subheader isn't used anywhere inline. * remove obsolete comment about GeoIP removal, since this info is seen as benefitical addition to the IP address * remove doubled newline: dietpi-print_large has an empty line included already. Adding a second empty line makes the gap too big IMO --- dietpi/func/dietpi-banner | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index e6416ac209..573625bb6a 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -320,8 +320,8 @@ $CLI_LINE" Print_Subheader() { - # ON THE CURRENT LINE Output green line separator with subheader title "$1" - echo -e "\r$CLI_LINE + # Output green line separator with subheader title "$1" + echo -e "$CLI_LINE ${aCOLOUR[strong]}$1 $CLI_SEPARATOR" } @@ -534,10 +534,6 @@ $CLI_LINE" if [[ $IP =~ ^$IP4_re ]] then # Valid IP detected, keep as-is and print with namespace - - # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. - #IP=${BASH_REMATCH[0]} - echo -e "${aCOLOUR[alt]} $IP [$display_ns]" elif [[ ! $IP ]] @@ -745,7 +741,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( Date: Thu, 9 Jul 2026 23:58:02 +0200 Subject: [PATCH 34/69] dietpi-banner: revert extra empty line Whoos, dietpi-print_large does NOT include an empty line. It just appears as if it did, if the hostname has not hanging character, like no "g", "q" etc. Those however make use of the last line, in which case the next banner entry looks too close. --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 573625bb6a..0b5e68c9d4 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -741,7 +741,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( Date: Fri, 10 Jul 2026 13:00:21 +0200 Subject: [PATCH 35/69] dietpi-banner: align comment with what the line looks like --- dietpi/func/dietpi-banner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0b5e68c9d4..26016141c3 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -649,8 +649,8 @@ $CLI_LINE" perc='?? %' # default unknown value fi - # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%b %b%-${length}s %b %b%6b %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" + # Print aligned columns: {basename} : ({percent used}) : {used} of {size} + printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" done } From 6cc71f6ecff604070a415ee080e97ae40a4ec53a Mon Sep 17 00:00:00 2001 From: timjolson Date: Thu, 9 Jul 2026 16:27:04 -0500 Subject: [PATCH 36/69] Made IP4_re mawk friendly. Fixed netns IPs not being detected due to being unfriendly. --- dietpi/func/dietpi-banner | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 26016141c3..5946f26114 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -182,8 +182,8 @@ CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" CLI_SEPARATOR="${aCOLOUR[accent]}:$COLOUR_RESET" - # IP address detection (IPv4) - IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' + # IP address detection (IPv4) - mawk-friendly (avoid + and {m,n}) + IP4_re='[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" @@ -512,7 +512,8 @@ $CLI_LINE" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9]{1,2}$" '$3 ~ re {print $3;exit}') + # Scan all fields for a matching IPv4/CIDR token. + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9][0-9]*$" '{for(i=1;i<=NF;i++) if($i ~ re){print $i; exit}}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue From 6bdd62cefd86870212edb2fa08376b61d48828ae Mon Sep 17 00:00:00 2001 From: timjolson Date: Thu, 9 Jul 2026 16:43:31 -0500 Subject: [PATCH 37/69] removed unused 'width' parameter --- dietpi/func/dietpi-banner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 5946f26114..b6c46536de 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -732,8 +732,8 @@ $CLI_LINE" { # Print a single line item with green bullet, description "$1", green separator and state "$2" # state should contain color and format codes as needed - local subtitle=$1 state=$2 width=$3 - printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" + local subtitle=$1 state=$2 + printf "%b%b %b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" } Print_Banner_raw() From ea320cfa4b12714732ec59f5d02e9b201a914d51 Mon Sep 17 00:00:00 2001 From: timjolson Date: Fri, 10 Jul 2026 12:10:27 -0500 Subject: [PATCH 38/69] Updated truncat doc --- CONTRIBUTING.md | 11 +++++++---- dietpi/func/dietpi-globals | 11 ++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29c28614bc..543fa35091 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,10 +203,13 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. ``` G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26 # -> "Long text to... the middle" - G_TRUNCATE_MID "Long text" 5 - # -> "L...t" - G_TRUNCATE_MID "Text" 3 - # -> "Tex" + + # And `G_TRUNCATE_MID "alphabetical" N` returns the below, as N decreases + # alp...al + # al...al + # alphab + # alpha + # ... and so on ``` - `Save()` persistence pattern (follow dietpi-banner conventions): diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index f49becd7eb..faf03d04ae 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -173,12 +173,17 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # Truncate a string in the middle to a maximum length, inserting "..." # - If the string is shorter than or equal to the maximum length, it is returned unchanged. - # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". + # - If the maximum length is less than 7, the string is truncated to the maximum length without adding "...". # - E.g. # For example: # - `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" - # - `G_TRUNCATE_MID "Long text" 5` returns "L...t" - # - `G_TRUNCATE_MID "Text" 3` returns "Tex" + # - `G_TRUNCATE_MID "G_TRUNCATE_MID "alphabetical" N` returns the below, as N decreases + # alp...al + # al...al + # alphab + # alpha + # ... and so on + G_TRUNCATE_MID() { local s=$1 max=$2 From 7cf10a10cf33c404a7bb4dbbd308c0fc5c22e86c Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 10 Jul 2026 20:10:33 +0200 Subject: [PATCH 39/69] dietpi-banner: re-apply fixed alignment for network stats I changed it to `echo -e` since the used `%s` format codes printed color codes literally. However, this can be solved with `%b` as well, keeping the min-length format codes of `printf`. Raised the "IP (iface)" min length to 23 characters, matchig complete IPv4 address + classic WiFi interface name. --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index b6c46536de..887cb19f8b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -517,7 +517,7 @@ $CLI_LINE" [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - echo -e "$CLI_BULLET ${aCOLOUR[weak]}$ip_addr ($ifname) $CLI_SEPARATOR ${aCOLOUR[weak]}TX=$TX RX=$RX" + printf '%b %b%-23s %b %bTX=%11s RX=%11s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" done } From bae63d589294a7f49f2c72c859ea87c58faa4e30 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 19 Jul 2026 18:40:00 -0500 Subject: [PATCH 40/69] Print_Disk_Usage now uses a single findmnt call and processes explicit paths before patterns --- dietpi/func/dietpi-banner | 185 +++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 83 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 887cb19f8b..f691291eaa 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -553,105 +553,124 @@ $CLI_LINE" Print_Disk_Usage() { - # If no arguments are provided, default to '"/" "/mnt/*"'. - (( $# )) && aDISK_SPACE_PATTERNS=("$@") + local length=0 results=() explicit_mounts=() match_patterns=() + local -A seen=() - # TODO: is there a more efficient reader than df (stat?) - # Use df's output to parse numeric fields (kB units) for mounts matching the supplied patterns - local length=0 results=() size_kb used_kb avail_kb mnt_target name matched basename - while read -r size_kb used_kb avail_kb mnt_target + # Split patterns: explicit (no glob, not regex) vs others (glob or re:) + for pattern in "${aDISK_SPACE_PATTERNS[@]}" do - matched=0 - basename= - for pattern in "${aDISK_SPACE_PATTERNS[@]}" - do - # If the user supplies a pattern prefixed with 're:' treat it as regex - if [[ $pattern == 're:'* ]] - then - re=${pattern#re:} - if [[ $mnt_target =~ $re ]] - then - matched=1 - basename=$(basename "$mnt_target") - break - fi - else - # Otherwise, treat it as a shell glob pattern and match using "case" - # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement - case "$mnt_target" in - $pattern) - matched=1 - basename=$(basename "$mnt_target") - break - ;; - *) :;; - esac - fi - done - if (( $matched )) + if [[ $pattern == 're:'* ]] then - len=${#basename} - (( $len > $length )) && length=$len - results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") - fi - done < <(df -k --output=size,used,avail,target | tail +2) - - # Print results with aligned columns, converting KiB to GiB and calculating percentage used - for entry in "${results[@]}" - do - IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" - - local values_count=0 - # Do all math in KiB, convert to GiB for display and round to 1 decimal - if [[ $size_kb =~ ^[0-9]+$ ]] + # This is a regex pattern, add to match_patterns + match_patterns+=("$pattern") + elif [[ $pattern == *'*'* || $pattern == *'?'* || $pattern == *'['* ]] then - ((values_count++)) - size_gb=$(mawk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + # This is a glob pattern, add to match_patterns + match_patterns+=("$pattern") else - if [[ $used_kb =~ ^[0-9]+$ && $avail_kb =~ ^[0-9]+$ ]] - then - # If used and available are available, calculate size as used + available - size_kb=$(( $used_kb + $avail_kb )) - size_gb=$(mawk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") - else - size_gb='???' - fi + # This is an explicit mount path, add to explicit_mounts + explicit_mounts+=("$pattern") fi + done + + if (( ${#explicit_mounts[@]} == 0 && ${#match_patterns[@]} == 0 )) + then + echo -e "${aCOLOUR[highlight]}No disk patterns provided, nothing to display." + return + fi - if [[ $used_kb =~ ^[0-9]+$ ]] + # Convert bytes to GiB (IEC) with one decimal — returns numeric string like "62.8" + Bytes_To_GiB() + { + local bytes=$1 + if [[ $bytes =~ ^[0-9]+$ ]] then - ((values_count++)) - used_gb=$(mawk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + # Use mawk for consistent floating-point formatting + mawk -v v="$bytes" 'BEGIN{printf "%.1f", v/(1024^3)}' else - if [[ $size_kb =~ ^[0-9]+$ && $avail_kb =~ ^[0-9]+$ ]] - then - # If size and available are available, calculate used as size - available - used_kb=$(( $size_kb - $avail_kb )) - used_gb=$(mawk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") - else - used_gb='???' - fi + echo '???' fi + } + + local -a mount_order=() mount_lines + declare -A mount_used mount_size mount_perc mount_name + local line mnt_target used size perc name + + # Pull all mounts once (-D -n -r) in bytes (-b). Rebuild TARGET safely + # (handles spaces) and compute USE% with one decimal using mawk from USED/SIZE. + mapfile -t mount_lines < <(findmnt -b -Dnro TARGET,USED,SIZE --real 2>/dev/null | mawk '{used=$(NF-1); size=$NF; target=$1; for(i=2;i<=NF-2;i++) target=target" "$i; perc=(size>0? used/size*100 : 0); printf "%s|%s|%s|%s\n", target, used, size, sprintf("%.1f%%", perc)}') + + # Keep mount_order[] to preserve findmnt's (consistent) order and aDISK_SPACE_PATTERNS' order. + for line in "${mount_lines[@]}" + do + # Each line is: TARGET|USED|SIZE|USE% + IFS='|' read -r mnt_target used size perc <<< "$line" + mount_used["$mnt_target"]="$used" + mount_size["$mnt_target"]="$size" + mount_perc["$mnt_target"]="$perc" + mount_name["$mnt_target"]="$(basename "$mnt_target")" + mount_order+=("$mnt_target") + done + + # Iterate mounts in the original findmnt order and match against patterns. + for mnt_target in "${mount_order[@]}" + do + # Skip mounts we've already added (avoids duplicates when patterns overlap) + [[ ${seen[$mnt_target]} ]] && continue + matched=0 - if [[ $avail_kb =~ ^[0-9]+$ ]] + # Test against explicit exact-match patterns (highest precedence) + for p in "${explicit_mounts[@]}" + do + [[ "$mnt_target" == "$p" ]] && { matched=1; break; } + done + + # If not matched yet, test globs and regex patterns from match_patterns + if (( matched == 0 )) then - ((values_count++)) - else - # If size and used are available, calculate available as size - used - [[ $size_kb =~ ^[0-9]+$ && $used_kb =~ ^[0-9]+$ ]] && avail_kb=$(( $size_kb - $used_kb )) + for pattern in "${match_patterns[@]}" + do + if [[ $pattern == 're:'* ]] + then + re=${pattern#re:} + if [[ $mnt_target =~ $re ]] + then + matched=1; break + fi + else + case "$mnt_target" in + $pattern) matched=1; break;; + *) :;; + esac + fi + done fi - # Calculate percentage used based on available data: - if (( $values_count > 1 )) + if (( matched )) then - # this should match the `df` output - perc=$(mawk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") - else - perc='?? %' # default unknown value + used="${mount_used[$mnt_target]}" + size="${mount_size[$mnt_target]}" + perc="${mount_perc[$mnt_target]}" + name="${mount_name[$mnt_target]}" + + # Convert raw bytes to GiB (IEC) numeric strings for display + used_fmt="$(Bytes_To_GiB "$used")" + size_fmt="$(Bytes_To_GiB "$size")" + + # Mark seen, update name-width and collect formatted result + seen[$mnt_target]=1 + (( ${#name} > length )) && length=${#name} + results+=("$mnt_target|$used_fmt|$size_fmt|$perc|$name") fi + done - # Print aligned columns: {basename} : ({percent used}) : {used} of {size} - printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" + # Print results using the user-friendly GiB strings collected above. + # Each `results` entry is: TARGET|USED_GiB|SIZE_GiB|USE%|NAME + for entry in "${results[@]}" + do + IFS='|' read -r mnt_target used size perc name <<< "$entry" + # Print aligned columns: {basename} : {percent used} : {used} of {size} + printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" done } @@ -780,7 +799,7 @@ $CLI_LINE" # Network usage by namespace (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 # Disk usage - (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1 + (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage 2>&1 # Custom commands (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands From f40f0ee84e8243d20fcb8c0724b5a85634fe1673 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 19 Jul 2026 19:01:59 -0500 Subject: [PATCH 41/69] fixed display typos --- dietpi/func/dietpi-banner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index f691291eaa..e423cd575f 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -67,7 +67,7 @@ [kernel]='Kernel' [network_usage]='Network usage' [disk_usage]='Disk usage' - [systemd]='systemd status' + [systemd]='Systemd status' [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up @@ -425,7 +425,7 @@ $CLI_LINE" elif (( $count_banned_ips > 20 )) then # high number of bans - state="${aCOLOUR[alert]}$count_banned_ips IP banned" + state="${aCOLOUR[alert]}$count_banned_ips IP(s) banned" else # low number of bans state="${aCOLOUR[weak]}$count_banned_ips IP(s) banned" From 0a7761fafad00508bea11590b68908c0b1aa5122 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 20 Jul 2026 16:51:21 -0500 Subject: [PATCH 42/69] split explicit disk mounts from patterns. de-duplicate exactly matching results. truncate disk display names --- dietpi/func/dietpi-banner | 75 +++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index e423cd575f..d3ed9d9441 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -118,8 +118,9 @@ # Set default custom patterns and commands # shellcheck disable=SC2016 aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"' - # Default disk patterns - aDISK_SPACE_PATTERNS=('/' '/mnt/dietpi_userdata') + # Default disk mounts (explicit paths) and patterns (globs/regex) + aDISK_SPACE_MOUNTS=() + aDISK_SPACE_PATTERNS=() # Load settings here, to have chosen custom CLI colours applied # shellcheck disable=SC1090 @@ -263,7 +264,15 @@ echo "aCOLOUR[$i]='$esc'" done - # Persist aDISK_SPACE_PATTERNS (indices 0..N) + # Persist aDISK_SPACE_MOUNTS (explicit mount paths, indices 0..N) + for i in "${!aDISK_SPACE_MOUNTS[@]}" + do + mount=${aDISK_SPACE_MOUNTS[$i]} + mount=${mount//\'/\\\'} + echo "aDISK_SPACE_MOUNTS[$i]='$mount'" + done + + # Persist aDISK_SPACE_PATTERNS (glob/regex patterns, indices 0..N) for i in "${!aDISK_SPACE_PATTERNS[@]}" do patt=${aDISK_SPACE_PATTERNS[$i]} @@ -553,27 +562,10 @@ $CLI_LINE" Print_Disk_Usage() { - local length=0 results=() explicit_mounts=() match_patterns=() + local results=() local -A seen=() - # Split patterns: explicit (no glob, not regex) vs others (glob or re:) - for pattern in "${aDISK_SPACE_PATTERNS[@]}" - do - if [[ $pattern == 're:'* ]] - then - # This is a regex pattern, add to match_patterns - match_patterns+=("$pattern") - elif [[ $pattern == *'*'* || $pattern == *'?'* || $pattern == *'['* ]] - then - # This is a glob pattern, add to match_patterns - match_patterns+=("$pattern") - else - # This is an explicit mount path, add to explicit_mounts - explicit_mounts+=("$pattern") - fi - done - - if (( ${#explicit_mounts[@]} == 0 && ${#match_patterns[@]} == 0 )) + if (( ${#aDISK_SPACE_MOUNTS[@]} == 0 && ${#aDISK_SPACE_PATTERNS[@]} == 0 )) then echo -e "${aCOLOUR[highlight]}No disk patterns provided, nothing to display." return @@ -620,7 +612,7 @@ $CLI_LINE" matched=0 # Test against explicit exact-match patterns (highest precedence) - for p in "${explicit_mounts[@]}" + for p in "${aDISK_SPACE_MOUNTS[@]}" do [[ "$mnt_target" == "$p" ]] && { matched=1; break; } done @@ -628,7 +620,7 @@ $CLI_LINE" # If not matched yet, test globs and regex patterns from match_patterns if (( matched == 0 )) then - for pattern in "${match_patterns[@]}" + for pattern in "${aDISK_SPACE_PATTERNS[@]}" do if [[ $pattern == 're:'* ]] then @@ -659,17 +651,48 @@ $CLI_LINE" # Mark seen, update name-width and collect formatted result seen[$mnt_target]=1 - (( ${#name} > length )) && length=${#name} results+=("$mnt_target|$used_fmt|$size_fmt|$perc|$name") fi done + if (( ${#results[@]} == 0 )) + then + echo -e "${aCOLOUR[highlight]}No mounts were matched, check your configuration." + return + fi + + # Disambiguate duplicate basenames: if multiple mounts share the same + # basename (e.g. /tmp and /mnt/tmp), show the full path for those entries + # so the user can tell them apart. Compute display widths accordingly. + declare -A name_count=() + for entry in "${results[@]}" + do + IFS='|' read -r mnt_target used size perc name <<< "$entry" + (( name_count["$name"]++ )) + done + + # Compute `length` based on the display name (basename or full path) + length=0 + for i in "${!results[@]}" + do + entry=${results[$i]} + IFS='|' read -r mnt_target used size perc name <<< "$entry" + if (( name_count["$name"] > 1 )) + then + dname="$(G_TRUNCATE_MID "$mnt_target" 30)" + else + dname="$(G_TRUNCATE_MID "$name" 30)" + fi + (( ${#dname} > length )) && length=${#dname} + results[$i]="$mnt_target|$used|$size|$perc|$dname" + done + # Print results using the user-friendly GiB strings collected above. # Each `results` entry is: TARGET|USED_GiB|SIZE_GiB|USE%|NAME for entry in "${results[@]}" do IFS='|' read -r mnt_target used size perc name <<< "$entry" - # Print aligned columns: {basename} : {percent used} : {used} of {size} + # Print aligned columns: {name} : {percent used} : {used} of {size} printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" done } From 914ce8664d3302237c01fa6dc105a09613050718 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 20 Jul 2026 17:22:53 -0500 Subject: [PATCH 43/69] added configured but not matched mounts to the disk usage output. used a safer delimiter --- dietpi/func/dietpi-banner | 40 ++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index d3ed9d9441..78e548470c 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -590,13 +590,14 @@ $CLI_LINE" # Pull all mounts once (-D -n -r) in bytes (-b). Rebuild TARGET safely # (handles spaces) and compute USE% with one decimal using mawk from USED/SIZE. - mapfile -t mount_lines < <(findmnt -b -Dnro TARGET,USED,SIZE --real 2>/dev/null | mawk '{used=$(NF-1); size=$NF; target=$1; for(i=2;i<=NF-2;i++) target=target" "$i; perc=(size>0? used/size*100 : 0); printf "%s|%s|%s|%s\n", target, used, size, sprintf("%.1f%%", perc)}') + # Use ASCII Unit Separator (0x1F) as a safe field delimiter to avoid collisions with mount names + mapfile -t mount_lines < <(findmnt -b -Dnro TARGET,USED,SIZE --real 2>/dev/null | mawk -v SEP="$(printf '\037')" '{used=$(NF-1); size=$NF; target=$1; for(i=2;i<=NF-2;i++) target=target" "$i; perc=(size>0? used/size*100 : 0); printf "%s%s%s%s%s%s%s\n", target, SEP, used, SEP, size, SEP, sprintf("%.1f%%", perc)}') # Keep mount_order[] to preserve findmnt's (consistent) order and aDISK_SPACE_PATTERNS' order. for line in "${mount_lines[@]}" do - # Each line is: TARGET|USED|SIZE|USE% - IFS='|' read -r mnt_target used size perc <<< "$line" + # Each line fields are separated by ASCII Unit Separator (0x1F): TARGETUSEDSIZEUSE% + IFS=$'\x1f' read -r mnt_target used size perc <<< "$line" mount_used["$mnt_target"]="$used" mount_size["$mnt_target"]="$size" mount_perc["$mnt_target"]="$perc" @@ -607,8 +608,6 @@ $CLI_LINE" # Iterate mounts in the original findmnt order and match against patterns. for mnt_target in "${mount_order[@]}" do - # Skip mounts we've already added (avoids duplicates when patterns overlap) - [[ ${seen[$mnt_target]} ]] && continue matched=0 # Test against explicit exact-match patterns (highest precedence) @@ -630,6 +629,7 @@ $CLI_LINE" matched=1; break fi else + # shellcheck disable=SC2254 case "$mnt_target" in $pattern) matched=1; break;; *) :;; @@ -649,9 +649,9 @@ $CLI_LINE" used_fmt="$(Bytes_To_GiB "$used")" size_fmt="$(Bytes_To_GiB "$size")" - # Mark seen, update name-width and collect formatted result + # Mark seen and collect formatted result (fields separated by ASCII Unit Separator) seen[$mnt_target]=1 - results+=("$mnt_target|$used_fmt|$size_fmt|$perc|$name") + results+=("$mnt_target"$'\x1f'"$used_fmt"$'\x1f'"$size_fmt"$'\x1f'"$perc"$'\x1f'"$name") fi done @@ -661,22 +661,33 @@ $CLI_LINE" return fi + # For explicit mounts configured but not present in findmnt, add placeholder entries + # at the bottom of the list. + for p in "${aDISK_SPACE_MOUNTS[@]}" + do + [[ -n "$p" && -z ${seen[$p]} ]] || continue + used_fmt="???" + size_fmt="???" + perc="${aCOLOUR[alert]}??????${COLOUR_RESET}" + name="$(basename "$p")" + results+=("$p"$'\x1f'"$used_fmt"$'\x1f'"$size_fmt"$'\x1f'"$perc"$'\x1f'"$name") + done + # Disambiguate duplicate basenames: if multiple mounts share the same # basename (e.g. /tmp and /mnt/tmp), show the full path for those entries # so the user can tell them apart. Compute display widths accordingly. declare -A name_count=() for entry in "${results[@]}" do - IFS='|' read -r mnt_target used size perc name <<< "$entry" + IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" (( name_count["$name"]++ )) done - # Compute `length` based on the display name (basename or full path) length=0 for i in "${!results[@]}" do entry=${results[$i]} - IFS='|' read -r mnt_target used size perc name <<< "$entry" + IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" if (( name_count["$name"] > 1 )) then dname="$(G_TRUNCATE_MID "$mnt_target" 30)" @@ -684,16 +695,15 @@ $CLI_LINE" dname="$(G_TRUNCATE_MID "$name" 30)" fi (( ${#dname} > length )) && length=${#dname} - results[$i]="$mnt_target|$used|$size|$perc|$dname" + results[$i]="$mnt_target"$'\x1f'"$used"$'\x1f'"$size"$'\x1f'"$perc"$'\x1f'"$dname" done - # Print results using the user-friendly GiB strings collected above. - # Each `results` entry is: TARGET|USED_GiB|SIZE_GiB|USE%|NAME + # Print results for entry in "${results[@]}" do - IFS='|' read -r mnt_target used size perc name <<< "$entry" + IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" # Print aligned columns: {name} : {percent used} : {used} of {size} - printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" + printf "%b %b%-${length}s %b %b%6b %b %b%7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" done } From 78a8045e095f4fe3613bd6f7e7e03ced23e98e62 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 20 Jul 2026 17:23:15 -0500 Subject: [PATCH 44/69] re-used Bytes_To_GiB --- dietpi/func/dietpi-banner | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 78e548470c..c7fc56e3de 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -448,6 +448,19 @@ $CLI_LINE" echo "$state" } + # Convert bytes to GiB (IEC) with one decimal — returns numeric string like "62.8" + Bytes_To_GiB() + { + local bytes=$1 + if [[ $bytes =~ ^[0-9]+$ ]] + then + # Use mawk for consistent floating-point formatting + mawk -v v="$bytes" 'BEGIN{printf "%.1f", v/(1024*1024*1024)}' + else + echo -e "${aCOLOUR[highlight]}???${COLOUR_RESET}" + fi + } + Print_Network_Usage() { @@ -517,8 +530,8 @@ $CLI_LINE" fi # Process the RX/TX values to convert to GiB if they are numeric, otherwise leave as 'N/A' - [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + [[ $rx =~ ^[0-9]+$ ]] && RX="$(Bytes_To_GiB "$rx") GiB" + [[ $tx =~ ^[0-9]+$ ]] && TX="$(Bytes_To_GiB "$tx") GiB" # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". # Scan all fields for a matching IPv4/CIDR token. From 77bc1e3520344266b3f4e2fddf27516bfb6d7196 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 20 Jul 2026 17:23:15 -0500 Subject: [PATCH 45/69] re-used Bytes_To_GiB --- dietpi/func/dietpi-banner | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c7fc56e3de..85a6638bdb 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -584,18 +584,6 @@ $CLI_LINE" return fi - # Convert bytes to GiB (IEC) with one decimal — returns numeric string like "62.8" - Bytes_To_GiB() - { - local bytes=$1 - if [[ $bytes =~ ^[0-9]+$ ]] - then - # Use mawk for consistent floating-point formatting - mawk -v v="$bytes" 'BEGIN{printf "%.1f", v/(1024^3)}' - else - echo '???' - fi - } local -a mount_order=() mount_lines declare -A mount_used mount_size mount_perc mount_name From 75343bd7f92adcc07007879dcf20a786e4fc7b69 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 20 Jul 2026 21:50:37 -0500 Subject: [PATCH 46/69] upgraded disk usage menu to list available mounts. simplified disk pattern submenu. added migration of disk_rootfs and disk_userdata (removed them from the menu). --- dietpi/func/dietpi-banner | 212 +++++++++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 50 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 85a6638bdb..25db37ee61 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -51,8 +51,6 @@ [nis_domainname]='NIS domainname' [lan_ip]='LAN IP' [wan_ip]='WAN IP' - [disk_rootfs]='Disk usage (RootFS)' - [disk_userdata]='Disk usage (userdata)' [weather]='Weather (wttr.in)' [custom_commands]='Custom commands' [dietpi_commands]='Display useful DietPi commands?' @@ -67,13 +65,14 @@ [kernel]='Kernel' [network_usage]='Network usage' [disk_usage]='Disk usage' + [disk_patterns]='does not show in the menu. allows disk patterns to be enabled/disabled' [systemd]='Systemd status' [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname - lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd fail2ban + lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban network_usage disk_usage custom_commands dietpi_commands motd credits word_wrap ) @@ -87,7 +86,7 @@ # Set default options aENABLED[device_model]=1 aENABLED[lan_ip]=1 - aENABLED[disk_rootfs]=1 + aENABLED[disk_usage]=1 aENABLED[dietpi_commands]=1 aENABLED[motd]=1 aENABLED[credits]=1 @@ -154,6 +153,28 @@ DIETPI_BANNER_MIGRATED_DURING_RUN=1 done + # Upgrade disk display options + if [[ -v aENABLED[disk_rootfs] ]] + then + if (( aENABLED[disk_rootfs] )) + then + aDISK_SPACE_MOUNTS+=('/') + aENABLED[disk_usage]=1 + fi + unset "aENABLED[disk_rootfs]" + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + fi + if [[ -v aENABLED[disk_userdata] ]] + then + if (( aENABLED[disk_userdata] )) + then + aDISK_SPACE_MOUNTS+=('/mnt/dietpi_userdata') + aENABLED[disk_usage]=1 + fi + unset "aENABLED[disk_userdata]" + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + fi + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form local legacy_indexed_colour_keys=(accent strong weak alert good highlight alt) for k in "${!aCOLOUR[@]}" @@ -457,7 +478,7 @@ $CLI_LINE" # Use mawk for consistent floating-point formatting mawk -v v="$bytes" 'BEGIN{printf "%.1f", v/(1024*1024*1024)}' else - echo -e "${aCOLOUR[highlight]}???${COLOUR_RESET}" + echo -e "???" fi } @@ -573,18 +594,79 @@ $CLI_LINE" done } + # Manage disk space pattern list (globs / regex) + Manage_Disk_Patterns() + { + local pattern_hint sel idx + + # Multi-line help text for the patterns submenu + pattern_hint="To use regex, start the pattern with 're:', otherwise shell globbing is used. +For example: '/mnt/*' or 're:^/mnt/.*/*$'" + + while : + do + # Build menu: Add first, then each active pattern, then Done + G_WHIP_MENU_ARRAY=() + G_WHIP_MENU_ARRAY+=( 'Add' 'Add a new pattern' ) + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do + G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}") + done + G_WHIP_MENU_ARRAY+=( 'Done' 'Finish pattern configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU "Manage disk space match patterns. The matching mounts returned by 'findmnt' will be reported.\n$pattern_hint" || break + + sel=$G_WHIP_RETURNED_VALUE + case "$sel" in + Add) + G_WHIP_DEFAULT_ITEM='' + G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a pattern + G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' + G_WHIP_INPUTBOX "$pattern_hint\n\nEdit pattern (empty removes it):" || continue + aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") + ;; + Done) + break + ;; + *) + # Selected an existing pattern index -> edit in-place + if [[ $sel =~ ^[0-9]+$ && -n "${aDISK_SPACE_PATTERNS[$sel]:-}" ]] + then + G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} + G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a pattern + G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' + G_WHIP_INPUTBOX "$pattern_hint\n\nEdit pattern (empty removes it):" + aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + else + # Unknown selection, ignore + continue + fi + ;; + esac + + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do + # Remove any empty entries (cleared by user) + [[ -n "${aDISK_SPACE_PATTERNS[$idx]:-}" ]] || unset 'aDISK_SPACE_PATTERNS[$idx]' + done + done + + # Re-index the array to remove gaps caused by unset + aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") + + # Disable pattern matching if there are none to match + (( ${#aDISK_SPACE_PATTERNS[@]} > 0 )) && aENABLED[disk_patterns]=1 + } + Print_Disk_Usage() { local results=() local -A seen=() - if (( ${#aDISK_SPACE_MOUNTS[@]} == 0 && ${#aDISK_SPACE_PATTERNS[@]} == 0 )) + if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( ${aENABLED[disk_patterns]} == 0 ) )) && (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) then echo -e "${aCOLOUR[highlight]}No disk patterns provided, nothing to display." return fi - local -a mount_order=() mount_lines declare -A mount_used mount_size mount_perc mount_name local line mnt_target used size perc name @@ -609,7 +691,7 @@ $CLI_LINE" # Iterate mounts in the original findmnt order and match against patterns. for mnt_target in "${mount_order[@]}" do - matched=0 + local matched=0 # Test against explicit exact-match patterns (highest precedence) for p in "${aDISK_SPACE_MOUNTS[@]}" @@ -617,8 +699,8 @@ $CLI_LINE" [[ "$mnt_target" == "$p" ]] && { matched=1; break; } done - # If not matched yet, test globs and regex patterns from match_patterns - if (( matched == 0 )) + # If not matched yet, test globs and regex patterns from match_patterns if enabled + if (( matched == 0 )) && (( aENABLED[disk_patterns] == 1 )) then for pattern in "${aDISK_SPACE_PATTERNS[@]}" do @@ -669,7 +751,7 @@ $CLI_LINE" [[ -n "$p" && -z ${seen[$p]} ]] || continue used_fmt="???" size_fmt="???" - perc="${aCOLOUR[alert]}??????${COLOUR_RESET}" + perc="???" name="$(basename "$p")" results+=("$p"$'\x1f'"$used_fmt"$'\x1f'"$size_fmt"$'\x1f'"$perc"$'\x1f'"$name") done @@ -684,7 +766,7 @@ $CLI_LINE" (( name_count["$name"]++ )) done - length=0 + local length=0 for i in "${!results[@]}" do entry=${results[$i]} @@ -855,7 +937,7 @@ $CLI_LINE" (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors were migrated from legacy numeric indices during this run - (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } Print_Banner() @@ -963,48 +1045,78 @@ $CLI_LINE" then # ignore check for single/double quote expansion issue; single/double quotes for glob; # shellcheck disable=SC2016,SC2086 - pattern_hint="The matching mount 'target's returned by the 'df' command will be reported. + pattern_hint="The matching mount 'target's returned by the 'findmnt' command will be reported. To use regex, start the pattern with 're:', otherwise shell globbing is used. For example: '/mnt/*' or 're:^/mnt/.*/*$'" - # Manage aDISK_SPACE_PATTERNS list - while : - do - G_WHIP_MENU_ARRAY=( 'Add' 'Add a new pattern' 'Edit' 'Edit an existing pattern' 'Remove' 'Remove a pattern' 'Done' 'Finish pattern configuration' ) - G_WHIP_DEFAULT_ITEM='Done' - G_WHIP_MENU "Manage disk space match patterns. $pattern_hint" || break - case "$G_WHIP_RETURNED_VALUE" in - Add) - G_WHIP_INPUTBOX "Enter new pattern:\n$pattern_hint" || continue - aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") - ;; - Edit) - (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to edit'; continue; } - G_WHIP_MENU_ARRAY=() - for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done - G_WHIP_MENU 'Select pattern to edit:' || continue - sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} - G_WHIP_INPUTBOX "Edit pattern:\n$pattern_hint" || continue - aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" - ;; - Remove) - (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to remove'; continue; } - G_WHIP_MENU_ARRAY=() - for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done - G_WHIP_MENU 'Select pattern to remove:' || continue - sel=$G_WHIP_RETURNED_VALUE - unset 'aDISK_SPACE_PATTERNS[$sel]' - # reindex array - aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") - ;; - Done) break;; - *) ;; - esac + # Build mount list from findmnt (preserve findmnt order) + local -a available_mounts + mapfile -t available_mounts < <(findmnt -Dnro TARGET --real 2>/dev/null) + # Build quick lookup maps used to determine which mounts are present + # and which are configured by the user: + # - `avail_map`: mounts currently detected on the system + # - `cfg_map` : mounts the user has previously configured to track + declare -A avail_map=() cfg_map=() + for m in "${available_mounts[@]}"; do avail_map["$m"]=1; done + for p in "${aDISK_SPACE_MOUNTS[@]}"; do cfg_map["$p"]=1; done + + # Prepare the checklist array and a mapping from generated tags back to + # mount paths. Entries are added in this order: + # 1) configured-but-missing mounts (so the user notices them), + # 2) a 'Custom matching' toggle, and + # 3) currently available mounts. + G_WHIP_CHECKLIST_ARRAY=() + declare -A tag2mount=() + local idx=0 tag desc default + + # Add configured-but-missing mounts at the top. + # Tags are created to be safe for G_WHIP_CHECKLIST (no spaces, no special characters). + # The tag is used to map back to the mount path. + for p in "${aDISK_SPACE_MOUNTS[@]}"; do + [[ -n "$p" && -z ${avail_map[$p]} ]] || continue + tag="m$idx" + desc="[not found] $p" + default='on' + G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") + tag2mount["$tag"]="$p" + ((idx++)) done - # If no patterns are configured, disable the disk usage option - (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && aENABLED[disk_usage]=0 + # Add Custom matching option to allow adding user patterns. + local custom_tag='CUSTOM_MATCHING' + if (( ${aENABLED[disk_patterns]} )); then default='on'; else default='off'; fi + G_WHIP_CHECKLIST_ARRAY+=("$custom_tag" "Custom matching (globs/regex patterns)" "$default") + + # Add available mounts from `findmnt` to the list. + for m in "${available_mounts[@]}"; do + tag="m$idx" + desc="$m" + if [[ ${cfg_map[$m]} ]]; then default='on'; else default='off'; fi + G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") + tag2mount["$tag"]="$m" + ((idx++)) + done + + # Display checklist (enumerated) + G_WHIP_CHECKLIST_ENUM=1 + G_WHIP_CHECKLIST "Select mounts to track. Use 'Custom matching' to manage globs/regex patterns." + + local sel + aENABLED[disk_patterns]=0 # Manage_Disk_Patterns sets to 1 if applicable + aDISK_SPACE_MOUNTS=() # Reset the mount list, it will be re-filled based on user selection below + + # Process selections: G_WHIP_RETURNED_VALUE contains selected tags + for sel in $G_WHIP_RETURNED_VALUE; do + if [[ $sel == "$custom_tag" ]]; then Manage_Disk_Patterns; continue; fi + if [[ ${tag2mount[$sel]} ]]; then aDISK_SPACE_MOUNTS+=("${tag2mount[$sel]}"); fi + done + + # If no disks are configured, disable the disk usage option + if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( aENABLED[disk_patterns] == 0 ) )) + then + (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 + fi + fi done From 42b2ce89fb0e49f5dcecd3e57995edfe7d986f1c Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 21 Jul 2026 12:02:41 -0500 Subject: [PATCH 47/69] added transient migration (every run) of .dietpi-banner_custom; switched aCUSTOM_COMMANDS to use associative indices to take advantage of Print_Item_State for display consistency; still TODO: update the submenu for associative indexing --- dietpi/func/dietpi-banner | 72 +++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 25db37ee61..c052c1a088 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -114,9 +114,11 @@ BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Set default custom patterns and commands + # Set default custom patterns and commands. If the user has a legacy .dietpi-banner_custom file, + # it will be migrated into aCUSTOM_COMMANDS later. # shellcheck disable=SC2016 - aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"' + declare -A aCUSTOM_COMMANDS=() + # Default disk mounts (explicit paths) and patterns (globs/regex) aDISK_SPACE_MOUNTS=() aDISK_SPACE_PATTERNS=() @@ -125,6 +127,21 @@ # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + # Set aCUSTOM_COMMANDS_ORDER from the assignments in the prefs file. + # Runtime order of custom command keys (stores user-defined order for display, saving, and menu). + aCUSTOM_COMMANDS_ORDER=() + + if [[ -f $FP_SAVEFILE ]]; then + while IFS= read -r line; do + # match patterns like: aCUSTOM_COMMANDS['key']='value' OR aCUSTOM_COMMANDS[0]='value' + if [[ $line =~ ^aCUSTOM_COMMANDS\[\'([^\']+)\'\]= ]]; then + aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") + elif [[ $line =~ ^aCUSTOM_COMMANDS\[([0-9]+)\]= ]]; then + aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") + fi + done < "$FP_SAVEFILE" + fi + # ---------------------------------------------------------------------------- # Legacy config migration helper # ---------------------------------------------------------------------------- @@ -153,7 +170,7 @@ DIETPI_BANNER_MIGRATED_DURING_RUN=1 done - # Upgrade disk display options + # Migrate legacy disk display options if [[ -v aENABLED[disk_rootfs] ]] then if (( aENABLED[disk_rootfs] )) @@ -175,6 +192,18 @@ DIETPI_BANNER_MIGRATED_DURING_RUN=1 fi + # Migrate legacy custom command file to aCUSTOM_COMMANDS array if present + readonly FP_CUSTOM='/boot/dietpi/.dietpi-banner_custom' + if [[ -f $FP_CUSTOM ]] + then + migrated_command_key="migrated-$(date '+%Y-%m-%d_%H-%M-%S')" + # Add a custom command that runs the external file with bash + aCUSTOM_COMMANDS=([$migrated_command_key]="bash \"$FP_CUSTOM\"") + aCUSTOM_COMMANDS_ORDER=("$migrated_command_key") + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=1 + fi + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form local legacy_indexed_colour_keys=(accent strong weak alert good highlight alt) for k in "${!aCOLOUR[@]}" @@ -195,6 +224,7 @@ } # Flag indicating whether a legacy numeric->associative config migration happened during this run DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=0 # Run migration DIETPI_BANNER_LEGACY_MIGRATION # ---------------------------------------------------------------------------- @@ -301,12 +331,22 @@ echo "aDISK_SPACE_PATTERNS[$i]='$patt'" done - # Persist aCUSTOM_COMMANDS (indices 0..N) - for i in "${!aCUSTOM_COMMANDS[@]}" + # Don't save a migrated custom command script + if (( DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) + then + unset "aCUSTOM_COMMANDS[$migrated_command_key]" + fi + + # Persist aCUSTOM_COMMANDS (associative indices) in user-specified order + for i in "${aCUSTOM_COMMANDS_ORDER[@]}" do + [[ -n "${aCUSTOM_COMMANDS[$i]:-}" ]] || continue + # Escape single quotes in the key and value for safe embedding + key=${i} + key=${key//\'/\\\'} cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} - echo "aCUSTOM_COMMANDS[$i]='$cmd'" + echo "aCUSTOM_COMMANDS['$key']='$cmd'" done # Persist BW indent settings @@ -315,6 +355,7 @@ # Clear migration flag now that prefs are persisted DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=0 } Print_Header() @@ -803,17 +844,18 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" Print_Custom_Commands() { - local output status - for i in "${!aCUSTOM_COMMANDS[@]}" + local key output status + for key in "${aCUSTOM_COMMANDS_ORDER[@]}" do - [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue - output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) + [[ -n "${aCUSTOM_COMMANDS[$key]:-}" ]] || continue + # Capture output and exit code + output=$(eval "${aCUSTOM_COMMANDS[$key]}" 2>&1) status=$? - if (( $status )) + if (( status )) then - echo -e "${aCOLOUR[alert]}Custom command $i failed ($status): $output" + Print_Item_State "$key" "${aCOLOUR[alert]}Command failed (code $status): $output${COLOUR_RESET}" else - echo -e "$output" + Print_Item_State "$key" "$output" fi done } @@ -867,8 +909,8 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" { # Print a single line item with green bullet, description "$1", green separator and state "$2" # state should contain color and format codes as needed - local subtitle=$1 state=$2 - printf "%b%b %b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" + local subtitle="$1" state="$2" + echo -e "$CLI_BULLET ${aCOLOUR[strong]} $subtitle $CLI_SEPARATOR $state$COLOUR_RESET" } Print_Banner_raw() From e9691021dcc8a2e13127a66c8ac31f5f04590431 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 21 Jul 2026 19:47:01 -0500 Subject: [PATCH 48/69] upgraded custom commands for associative array and Print_Item_State; upgraded custom commands submenu to match the style of disk_usage patterns submenu; added custom command migration (needs testing) --- dietpi/func/dietpi-banner | 383 +++++++++++++++++++++++--------------- 1 file changed, 236 insertions(+), 147 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c052c1a088..a9e38db744 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -635,8 +635,80 @@ $CLI_LINE" done } + Menu_Disk_Mounts() + { + # ignore check for single/double quote expansion issue; single/double quotes for glob; + # shellcheck disable=SC2016,SC2086 + pattern_hint="The matching mount 'target's returned by the 'findmnt' command will be reported. +To use regex, start the pattern with 're:', otherwise shell globbing is used. +For example: '/mnt/*' or 're:^/mnt/.*/*$'" + + # Build mount list from findmnt (preserve findmnt order) + local -a available_mounts + mapfile -t available_mounts < <(findmnt -Dnro TARGET --real 2>/dev/null) + # Build quick lookup maps used to determine which mounts are present + # and which are configured by the user: + # - `avail_map`: mounts currently detected on the system + # - `cfg_map` : mounts the user has previously configured to track + declare -A avail_map=() cfg_map=() + for m in "${available_mounts[@]}"; do avail_map["$m"]=1; done + for p in "${aDISK_SPACE_MOUNTS[@]}"; do cfg_map["$p"]=1; done + + # Prepare the checklist array and a mapping from generated tags back to + # mount paths. Entries are added in this order: + # 1) configured-but-missing mounts (so the user notices them), + # 2) a 'Custom matching' toggle, and + # 3) currently available mounts. + G_WHIP_CHECKLIST_ARRAY=() + declare -A tag2mount=() + local idx=0 tag desc default + + # Add configured-but-missing mounts at the top. + # Tags are created to be safe for G_WHIP_CHECKLIST (no spaces, no special characters). + # The tag is used to map back to the mount path. + for p in "${aDISK_SPACE_MOUNTS[@]}"; do + [[ -n "$p" && -z ${avail_map[$p]} ]] || continue + tag="m$idx" + desc="[not found] $p" + default='on' + G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") + tag2mount["$tag"]="$p" + ((idx++)) + done + + # Add Custom matching option to allow adding user patterns. + local custom_tag='CUSTOM_MATCHING' + if (( ${aENABLED[disk_patterns]} )); then default='on'; else default='off'; fi + G_WHIP_CHECKLIST_ARRAY+=("$custom_tag" "Custom matching (globs/regex patterns)" "$default") + + # Add available mounts from `findmnt` to the list. + for m in "${available_mounts[@]}"; do + tag="m$idx" + desc="$m" + if [[ ${cfg_map[$m]} ]]; then default='on'; else default='off'; fi + G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") + tag2mount["$tag"]="$m" + ((idx++)) + done + + # Display checklist (enumerated) + G_WHIP_CHECKLIST_ENUM=1 + G_WHIP_CHECKLIST "Select mounts to track. Use 'Custom matching' to manage globs/regex patterns." + + local sel + aENABLED[disk_patterns]=0 # Manage_Disk_Patterns sets to 1 if applicable + aDISK_SPACE_MOUNTS=() # Reset the mount list, it will be re-filled based on user selection below + + # Process selections: G_WHIP_RETURNED_VALUE contains selected tags. + # Rebuild mounts array and call Manage_Disk_Patterns if the user selected the custom matching option. + for sel in $G_WHIP_RETURNED_VALUE; do + if [[ $sel == "$custom_tag" ]]; then Menu_Disk_Patterns; continue; fi + if [[ ${tag2mount[$sel]} ]]; then aDISK_SPACE_MOUNTS+=("${tag2mount[$sel]}"); fi + done + } + # Manage disk space pattern list (globs / regex) - Manage_Disk_Patterns() + Menu_Disk_Patterns() { local pattern_hint sel idx @@ -693,7 +765,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Re-index the array to remove gaps caused by unset aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") - # Disable pattern matching if there are none to match + # If any patterns are configured, enable the disk_patterns option (( ${#aDISK_SPACE_PATTERNS[@]} > 0 )) && aENABLED[disk_patterns]=1 } @@ -842,6 +914,135 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" echo "$state" } + Menu_Custom_Commands() + { + local hint sel key cmd + + # Help text + # shellcheck disable=SC2016 + hint='Example:\necho -e "${aCOLOUR[good]}Hello World! :)"' + name_hint="Enter a name for the command. It must be unique, safe, and not empty." + + while : + do + # Build menu: `Add` first, then each active command, then `Done` + G_WHIP_MENU_ARRAY=() + G_WHIP_MENU_ARRAY+=( 'Add' 'Add a new pattern' ) + for key in "${aCUSTOM_COMMANDS_ORDER[@]}"; do + G_WHIP_MENU_ARRAY+=("$key" "${aCUSTOM_COMMANDS[$key]}") + done + G_WHIP_MENU_ARRAY+=( 'Done' 'Finish commands configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU "Manage custom commands. Each command gets a name and a corresponding shell command.\n$hint" || break + + sel=$G_WHIP_RETURNED_VALUE + case "$sel" in + Add) + # Add a new command + G_WHIP_DEFAULT_ITEM='' + G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a command + G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' + + G_WHIP_INPUTBOX "$hint\n\nEdit command (empty removes it):" || continue + cmd="$G_WHIP_RETURNED_VALUE" + + if [[ -z "$cmd" ]] + then + # Empty command, do not add to array + continue + else + G_WHIP_DEFAULT_ITEM='' + G_WHIP_INPUTBOX_REGEX='.+' # Non-empty + G_WHIP_INPUTBOX_REGEX_TEXT='must be unique, safe, and must not be empty' + + while :; + do + # New command entered, prompt for a name + G_WHIP_INPUTBOX "$name_hint" || continue + key="$G_WHIP_RETURNED_VALUE" + if [[ -n "${aCUSTOM_COMMANDS[$key]:-}" ]] + then + # Key already exists, prompt again + continue + else + # Key is new and valid, accept it + break + fi + done + aCUSTOM_COMMANDS+=([$key]="$cmd") + aCUSTOM_COMMANDS_ORDER+=("$key") + fi + ;; # End adding of command + Done) + break + ;; + *) + # Edit a command + if [[ -n "$sel" ]] + then + G_WHIP_DEFAULT_ITEM="${aCUSTOM_COMMANDS[$sel]}" + G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a command + G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' + G_WHIP_INPUTBOX "$hint\n\nEdit command (empty removes it):" + cmd="$G_WHIP_RETURNED_VALUE" + + if [[ -z "$cmd" ]] + then + # Empty command, remove from array + unset "aCUSTOM_COMMANDS[$sel]" + # Re-index array + for i in "${!aCUSTOM_COMMANDS_ORDER[@]}"; do + [[ "${aCUSTOM_COMMANDS_ORDER[$i]}" == "$sel" ]] && unset 'aCUSTOM_COMMANDS_ORDER[$i]' + done + aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") + continue + else + # New command entered, prompt for a name + G_WHIP_DEFAULT_ITEM="$sel" + G_WHIP_INPUTBOX_REGEX='.+' # Non-empty + G_WHIP_INPUTBOX_REGEX_TEXT='must be unique, safe, and must not be empty' + + while :; + do + G_WHIP_INPUTBOX "$name_hint" || continue + key="$G_WHIP_RETURNED_VALUE" + if [[ $key == "$sel" ]] + then + # Key unchanged, just update the command + aCUSTOM_COMMANDS[$key]="$cmd" + break + elif [[ -n "${aCUSTOM_COMMANDS[$key]:-}" && "$key" != "$sel" ]] + then + # Key changed to an existing key, prompt again + continue + else + # Key is new and valid, accept it + # Re-index array + for i in "${!aCUSTOM_COMMANDS_ORDER[@]}"; do + [[ "${aCUSTOM_COMMANDS_ORDER[$i]}" == "$sel" ]] && unset 'aCUSTOM_COMMANDS_ORDER[$i]' + done + aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") + unset "aCUSTOM_COMMANDS[$sel]" + + # Add the new key and command + aCUSTOM_COMMANDS+=([$key]="$cmd") + aCUSTOM_COMMANDS_ORDER+=("$key") + break + fi + done + fi + else + # Unknown selection, ignore + continue + fi + ;; # End editing of command + esac # End case "$sel" + done # End command menu loop + + # If any commands are configured, enable the custom_commands option + (( ${#aCUSTOM_COMMANDS_ORDER[@]} > 0 )) && aENABLED[custom_commands]=1 + } + Print_Custom_Commands() { local key output status @@ -942,10 +1143,6 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(G_GET_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" - # Disk usage (RootFS) - (( ${aENABLED[disk_rootfs]} )) && echo -e "$CLI_BULLET ${aCOLOUR[strong]}${aDESCRIPTION[disk_rootfs]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" - # Disk usage (DietPi userdata) - (( ${aENABLED[disk_userdata]} )) && echo -e "$CLI_BULLET ${aCOLOUR[strong]}${aDESCRIPTION[disk_userdata]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather (( ${aENABLED[weather]} )) && Print_Item_State "${aDESCRIPTION[weather]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status @@ -1015,151 +1212,43 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" for selection in $G_WHIP_RETURNED_VALUE do + # Enable options (the typical action for the checklist) aENABLED[$selection]=1 - if [[ $selection == 'custom_commands' ]] - then - # ignore check for single/double quote expansion issue; unassigned var is referenced; - # shellcheck disable=SC2016,SC2154 - example_command='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' - - # Manage custom commands list - while : - do + # Special actions for some items (like submenus) that require additional configuration + case $selection in + 'custom_commands') + Menu_Custom_Commands + # If no commands are configured, disable the custom commands option + (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 + ;; + 'word_wrap') + # Banner word-wrap options G_WHIP_MENU_ARRAY=( - 'Add' 'Add a new command' - 'Edit' 'Edit an existing command' - 'Remove' 'Remove a command' - 'Done' 'Finish custom commands configuration' + 'colon' 'Indent to the green colon if present, else to the green dash' + 'dash' 'Indent to the green dash character' + 'fixed' 'Indent to a fixed offset' ) - G_WHIP_DEFAULT_ITEM='Done' - G_WHIP_MENU 'Manage custom banner commands.\n\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit:' || break - case "$G_WHIP_RETURNED_VALUE" in - Add) - G_WHIP_INPUTBOX "Enter new command, for example:\n$example_command" || continue - aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") - ;; - Edit) - (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && { G_WHIP_MSG 'No commands to edit'; continue; } - G_WHIP_MENU_ARRAY=() - for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done - G_WHIP_MENU 'Select command to edit:' || continue - sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} - G_WHIP_INPUTBOX "Edit command, for example:\n$example_command" || continue - aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" - ;; - Remove) - (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && { G_WHIP_MSG 'No commands to remove'; continue; } - G_WHIP_MENU_ARRAY=() - for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done - G_WHIP_MENU 'Select command to remove:' || continue - sel=$G_WHIP_RETURNED_VALUE - unset 'aCUSTOM_COMMANDS[$sel]' - # reindex array - aCUSTOM_COMMANDS=("${aCUSTOM_COMMANDS[@]}") - ;; - Done) break;; - *) ;; - esac - done - - # If no commands are configured, disable the custom commands option - (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 - - elif [[ $selection == 'word_wrap' ]] - then - # Banner word-wrap options - G_WHIP_MENU_ARRAY=( - 'colon' 'Indent to the green colon if present, else to the green dash' - 'dash' 'Indent to the green dash character' - 'fixed' 'Indent to a fixed offset' - ) - G_WHIP_DEFAULT_ITEM=$BW_INDENT_TYPE - G_WHIP_MENU 'Choose how word-wrapped lines shall be indented:' || continue - BW_INDENT_TYPE=$G_WHIP_RETURNED_VALUE - [[ $BW_INDENT_TYPE == 'fixed' ]] || continue - G_WHIP_DEFAULT_ITEM=$BW_INDENT_FIXED - G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' - G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE - - elif [[ $selection == 'disk_usage' ]] - then - # ignore check for single/double quote expansion issue; single/double quotes for glob; - # shellcheck disable=SC2016,SC2086 - pattern_hint="The matching mount 'target's returned by the 'findmnt' command will be reported. -To use regex, start the pattern with 're:', otherwise shell globbing is used. -For example: '/mnt/*' or 're:^/mnt/.*/*$'" - - # Build mount list from findmnt (preserve findmnt order) - local -a available_mounts - mapfile -t available_mounts < <(findmnt -Dnro TARGET --real 2>/dev/null) - # Build quick lookup maps used to determine which mounts are present - # and which are configured by the user: - # - `avail_map`: mounts currently detected on the system - # - `cfg_map` : mounts the user has previously configured to track - declare -A avail_map=() cfg_map=() - for m in "${available_mounts[@]}"; do avail_map["$m"]=1; done - for p in "${aDISK_SPACE_MOUNTS[@]}"; do cfg_map["$p"]=1; done - - # Prepare the checklist array and a mapping from generated tags back to - # mount paths. Entries are added in this order: - # 1) configured-but-missing mounts (so the user notices them), - # 2) a 'Custom matching' toggle, and - # 3) currently available mounts. - G_WHIP_CHECKLIST_ARRAY=() - declare -A tag2mount=() - local idx=0 tag desc default - - # Add configured-but-missing mounts at the top. - # Tags are created to be safe for G_WHIP_CHECKLIST (no spaces, no special characters). - # The tag is used to map back to the mount path. - for p in "${aDISK_SPACE_MOUNTS[@]}"; do - [[ -n "$p" && -z ${avail_map[$p]} ]] || continue - tag="m$idx" - desc="[not found] $p" - default='on' - G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") - tag2mount["$tag"]="$p" - ((idx++)) - done - - # Add Custom matching option to allow adding user patterns. - local custom_tag='CUSTOM_MATCHING' - if (( ${aENABLED[disk_patterns]} )); then default='on'; else default='off'; fi - G_WHIP_CHECKLIST_ARRAY+=("$custom_tag" "Custom matching (globs/regex patterns)" "$default") - - # Add available mounts from `findmnt` to the list. - for m in "${available_mounts[@]}"; do - tag="m$idx" - desc="$m" - if [[ ${cfg_map[$m]} ]]; then default='on'; else default='off'; fi - G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") - tag2mount["$tag"]="$m" - ((idx++)) - done - - # Display checklist (enumerated) - G_WHIP_CHECKLIST_ENUM=1 - G_WHIP_CHECKLIST "Select mounts to track. Use 'Custom matching' to manage globs/regex patterns." - - local sel - aENABLED[disk_patterns]=0 # Manage_Disk_Patterns sets to 1 if applicable - aDISK_SPACE_MOUNTS=() # Reset the mount list, it will be re-filled based on user selection below - - # Process selections: G_WHIP_RETURNED_VALUE contains selected tags - for sel in $G_WHIP_RETURNED_VALUE; do - if [[ $sel == "$custom_tag" ]]; then Manage_Disk_Patterns; continue; fi - if [[ ${tag2mount[$sel]} ]]; then aDISK_SPACE_MOUNTS+=("${tag2mount[$sel]}"); fi - done - - # If no disks are configured, disable the disk usage option - if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( aENABLED[disk_patterns] == 0 ) )) - then - (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 - fi - - fi + G_WHIP_DEFAULT_ITEM=$BW_INDENT_TYPE + G_WHIP_MENU 'Choose how word-wrapped lines shall be indented:' || continue + BW_INDENT_TYPE=$G_WHIP_RETURNED_VALUE + [[ $BW_INDENT_TYPE == 'fixed' ]] || continue + G_WHIP_DEFAULT_ITEM=$BW_INDENT_FIXED + G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' + G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE + ;; + 'disk_usage') + Menu_Disk_Mounts + # If no disks are configured, disable the disk usage option + if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( aENABLED[disk_patterns] == 0 ) )) + then + (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 + fi + ;; + *) + G_DIETPI-NOTIFY 1 "Unknown menu selection: $selection" >&2 + ;; + esac done # Before saving, create a backup of the existing prefs if migrating. From 5bd6cb392c95c0e93ad4eeb14db1bac73a474022 Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 22 Jul 2026 11:32:23 -0500 Subject: [PATCH 49/69] added custom command migration warning --- dietpi/func/dietpi-banner | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a9e38db744..cf7ce5cf65 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -198,9 +198,8 @@ then migrated_command_key="migrated-$(date '+%Y-%m-%d_%H-%M-%S')" # Add a custom command that runs the external file with bash - aCUSTOM_COMMANDS=([$migrated_command_key]="bash \"$FP_CUSTOM\"") - aCUSTOM_COMMANDS_ORDER=("$migrated_command_key") - DIETPI_BANNER_MIGRATED_DURING_RUN=1 + aCUSTOM_COMMANDS+=([$migrated_command_key]="bash \"$FP_CUSTOM\"") + aCUSTOM_COMMANDS_ORDER+=("$migrated_command_key") DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=1 fi @@ -1111,7 +1110,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Print a single line item with green bullet, description "$1", green separator and state "$2" # state should contain color and format codes as needed local subtitle="$1" state="$2" - echo -e "$CLI_BULLET ${aCOLOUR[strong]} $subtitle $CLI_SEPARATOR $state$COLOUR_RESET" + echo -e "$CLI_BULLET${aCOLOUR[strong]} $subtitle $CLI_SEPARATOR $state$COLOUR_RESET" } Print_Banner_raw() @@ -1177,6 +1176,9 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Notify user if prefs or colors were migrated from legacy numeric indices during this run (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + (( DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) && echo -e " ${aCOLOUR[highlight]}Your custom command from '.dietpi-banner_custom' needs to be migrated. + Custom commands are now created through 'dietpi-banner'. You can run + a script as a command by configuring 'bash \"/path/to/script\"' .$COLOUR_RESET" } Print_Banner() From 559339954f252cf3f252ad10487ce1f573e26c62 Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 22 Jul 2026 11:51:03 -0500 Subject: [PATCH 50/69] Fixed migrated command not running after going through the menu. --- dietpi/func/dietpi-banner | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index cf7ce5cf65..1bb25d72fc 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -330,15 +330,12 @@ echo "aDISK_SPACE_PATTERNS[$i]='$patt'" done - # Don't save a migrated custom command script - if (( DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) - then - unset "aCUSTOM_COMMANDS[$migrated_command_key]" - fi - # Persist aCUSTOM_COMMANDS (associative indices) in user-specified order for i in "${aCUSTOM_COMMANDS_ORDER[@]}" do + # Skip a transient migrated command + [[ $i == "$migrated_command_key" ]] && continue + # Skip empty commands [[ -n "${aCUSTOM_COMMANDS[$i]:-}" ]] || continue # Escape single quotes in the key and value for safe embedding key=${i} From 2df5573f72b7077f11dccc11edb3f28a6579c472 Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 22 Jul 2026 11:51:30 -0500 Subject: [PATCH 51/69] quote custom command outputs for safety --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 1bb25d72fc..833ef68348 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -1046,7 +1046,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" do [[ -n "${aCUSTOM_COMMANDS[$key]:-}" ]] || continue # Capture output and exit code - output=$(eval "${aCUSTOM_COMMANDS[$key]}" 2>&1) + output="$(eval "${aCUSTOM_COMMANDS[$key]}" 2>&1)" status=$? if (( status )) then From 16fa623fd7204fd027d19addb6688b5a64638da1 Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 22 Jul 2026 11:54:37 -0500 Subject: [PATCH 52/69] keep migration warnings when user has gone through the menu and then the banner is displayed --- dietpi/func/dietpi-banner | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 833ef68348..fee5eb9d8f 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -348,10 +348,6 @@ # Persist BW indent settings echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" echo "BW_INDENT_FIXED=$BW_INDENT_FIXED" - - # Clear migration flag now that prefs are persisted - DIETPI_BANNER_MIGRATED_DURING_RUN=0 - DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=0 } Print_Header() From ed264333b46737f1117921cefb8c52591621e661 Mon Sep 17 00:00:00 2001 From: timjolson Date: Thu, 23 Jul 2026 13:42:00 -0500 Subject: [PATCH 53/69] cleaning up separator lines --- dietpi/func/dietpi-banner | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index fee5eb9d8f..c8bc9ba95c 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -72,8 +72,8 @@ # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname - lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban - network_usage disk_usage custom_commands dietpi_commands motd credits word_wrap + lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban motd + network_usage disk_usage custom_commands dietpi_commands credits word_wrap ) # Initialize aENABLED from aDESCRIPTION keys @@ -1143,16 +1143,6 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" (( ${aENABLED[systemd]} )) && Print_Item_State "${aDESCRIPTION[systemd]}" "$(Get_Systemd_Status 2>&1)" # Fail2Ban status (( ${aENABLED[fail2ban]} )) && Print_Item_State "${aDESCRIPTION[fail2ban]}" "$(Get_Fail2Ban_Status 2>&1)" - # Network usage by namespace - (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 - # Disk usage - (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage 2>&1 - # Custom commands - (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands - - # Separator line - (( ${aENABLED[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[credits]} )) && echo -e "$CLI_LINE" - # MOTD if (( ${aENABLED[motd]} )) then @@ -1161,11 +1151,23 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # shellcheck disable=SC1090 [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[motd]}" "$motd" fi - echo -e "$CLI_LINE\n" + + # Network usage by namespace + (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 + # Disk usage + (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage 2>&1 + # Custom commands + (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands + + # Show separator if any relevant follow-up sections or updates are present. + # Most flags are numeric (0/1); AVAILABLE_UPDATE is a version string when present. + if [[ -n "$AVAILABLE_UPDATE" ]] || (( aENABLED[dietpi_commands] || aENABLED[motd] || aENABLED[credits] || LIVE_PATCHES || PACKAGE_COUNT || REBOOT_REQUIRED )); then + echo -e "$CLI_LINE" + fi (( ${aENABLED[credits]} )) && Print_Credits - Print_Updates (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands + Print_Updates # Notify user if prefs or colors were migrated from legacy numeric indices during this run (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" @@ -1240,6 +1242,9 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 fi ;; + dietpi_commands|credits|motd|network_usage|letsencrypt|systemd|fail2ban|large_hostname|device_model|uptime|kernel|cpu_temp|ram_usage|load_average|hostname|nis_domainname|lan_ip|wan_ip|vpn_status|weather) + # No additional configuration needed for these options + ;; *) G_DIETPI-NOTIFY 1 "Unknown menu selection: $selection" >&2 ;; From fd9bbbacdf027defbf63bbbd3f4ce4a43ff4a5aa Mon Sep 17 00:00:00 2001 From: timjolson Date: Sat, 25 Jul 2026 10:34:26 -0500 Subject: [PATCH 54/69] LAN ip was not being printed with `dietpi-banner 0`. aENABLED check is handled in Print_Banner, and `dietpi-banner 0` should not care --- dietpi/func/dietpi-banner | 1 - 1 file changed, 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c8bc9ba95c..68ed57f734 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -390,7 +390,6 @@ $CLI_LINE" Get_Local_Ip() { - (( ${aENABLED[lan_ip]} )) || return 0 local iface=$(G_GET_NET -q iface) local ip=$(G_GET_NET -q ip) echo "${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" From 1d2961f339279c76737b8f22e77f82ae9098c1ca Mon Sep 17 00:00:00 2001 From: timjolson Date: Sat, 25 Jul 2026 10:35:57 -0500 Subject: [PATCH 55/69] reset text color after the banner --- dietpi/func/dietpi-banner | 1 + 1 file changed, 1 insertion(+) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 68ed57f734..d61bac83a5 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -1186,6 +1186,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" else Print_Banner_raw fi + echo -e "$COLOUR_RESET" } Menu_Main() From 29de14f1342f0d4e6cc77137032f8710941be84c Mon Sep 17 00:00:00 2001 From: timjolson Date: Sat, 25 Jul 2026 18:21:55 -0500 Subject: [PATCH 56/69] organized and updated CONTRIBUTING.md --- CONTRIBUTING.md | 114 +++++++++++++++++--------------------- dietpi/func/dietpi-banner | 2 +- 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 543fa35091..1779274fdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,13 +55,13 @@ Guidance: ### Core concepts - Globals: scripts source `dietpi/func/dietpi-globals` for shared helpers and - environment setup (`G_INIT`, `G_EXIT`, color vars, etc.). Read this file + environment setup (`G_INIT()`, `G_EXIT()`, color vars, etc.). Read this file first to understand helper semantics and cancel/error behavior. - UI: prefer `G_WHIP_*` dialog helpers for menus, input and confirmations to maintain a consistent user experience across scripts. -- Error-handling: use `G_EXEC` to wrap any command call, so DietPi's error +- Error-handling: use `G_EXEC()` to wrap any command call, so DietPi's error handler and consistent console output apply. Validate root permissions and - write access using `G_CHECK_ROOT_USER` / `G_CHECK_ROOTFS_RW` where required. + write access using `G_CHECK_ROOT_USER()` / `G_CHECK_ROOTFS_RW()` where required. - Persistence: Write arrays or variables into a preference file `/boot/dietpi/.`. Persist arrays as indexed assignments (e.g. `aARRAY[index]=1`). Convert ESC bytes to `\e` when saving text if needed. Load the preferences with `. "/boot/dietpi/."` @@ -76,65 +76,49 @@ DietPi provides many `G_` prefixed helpers in `dietpi/func/dietpi-globals`. Usag consistent and reduce reviewer friction. Below are the most useful ones for contributors and how to use them safely. -- `G_INIT` — initialize script runtime, sets up the working directory, exit +- `G_INIT()` — initialize script runtime, sets up the working directory, exit traps, consistent locale for parsing external command outputs, and handles concurrent execution checks. Call early after sourcing `dietpi-globals`. -- `G_EXEC` — robust command executor with built-in retries and an interactive +- `G_EXEC()` — robust command executor with built-in retries and an interactive error handler. Use instead of direct `rm`/`systemctl` in scripts so failures are presented to the user and logged consistently. Optional env vars: `$G_EXEC_DESC`, `$G_EXEC_RETRIES`, `$G_EXEC_OUTPUT`. -- `G_CONFIG_INJECT` — targeted config-file editing helper. Use to atomically - replace, uncomment, or add config lines using predictable patterns rather - than ad-hoc `sed` calls. - -- `G_WHIP_*` family — dialog and UI helpers: (`G_WHIP_MSG`, `G_WHIP_YESNO`, - `G_WHIP_MENU`, `G_WHIP_CHECKLIST`, `G_WHIP_INPUTBOX`, `G_WHIP_PASSWORD`, - `G_WHIP_VIEWFILE`). - Prefer these for user interaction to maintain consistent UX and behavior. - -- `G_CHECK_ROOT_USER`, `G_CHECK_ROOTFS_RW` — validate that the script runs +- `G_CHECK_ROOT_USER()`, `G_CHECK_ROOTFS_RW()` — validate that the script runs with necessary privileges and writable rootfs before performing writes. Using `G_CHECK_ROOT_USER "$@"`, if the script does not have root permissions, re-executes with `sudo`. "$@" passes all CLI arguments to the `sudo-ed` script. -- `G_GET_NET`, `G_GET_WAN_IP` — network helpers that return standardized +- `G_CONFIG_INJECT()` — targeted config-file editing helper. Use to atomically + replace, uncomment, or add config lines using predictable patterns rather + than ad-hoc `sed` calls. + +- `G_GET_NET()`, `G_GET_WAN_IP()` — network helpers that return standardized values; use `-q` to hide error messages. -- `G_DIETPI-NOTIFY` / `G_BUG_REPORT` — helpers to generate formatted bug +- `G_DIETPI-NOTIFY()` / `G_BUG_REPORT()` — helpers to generate formatted bug reports and diagnostics. Use when capturing logs for PRs / issues. -#### G_WHIP specifics - -These details are commonly needed when implementing menus and input boxes. - -- `G_WHIP_INPUTBOX`: - - Use `$G_WHIP_INPUTBOX_REGEX` to provide an optional validation regex and - `$G_WHIP_INPUTBOX_REGEX_TEXT` to describe allowed input (human-friendly). - By default, any non-empty input is allowed. - - `$G_WHIP_DEFAULT_ITEM` pre-fills the input field. The helper loops until - input matches the regex or the user cancels (`|| return`). - - On success the entered value is returned in `$G_WHIP_RETURNED_VALUE`. - -- `G_WHIP_YESNO`: - - Presents a Yes/No dialog. Exit status `0` indicates Yes/Ok; non-zero is - No/Cancel. - - Use for confirmations before destructive actions. Combine with - `G_EXEC` for safe command execution on confirmation. - -- `$G_WHIP_DEFAULT_ITEM`: - - Controls the pre-selected menu item or prefilled input box value. - - When using `G_WHIP_MENU`, set it to a label matching one of the menu - entries to pre-select that entry (exact match is used). - -- `$G_WHIP_SIZE_X_MAX`: - - Optional integer to limit dialog width (chars). Useful for dialogs with - very few content or checklists, where too wide dialog boxes may look weird. - By default, all `G_WHIP` dialogs are max 120 characters wide, capped by the - terminal width. - - Set it before calling a `G_WHIP_*` helper; the helper respects it when - calculating `$WHIP_SIZE_X`. +- `G_WHIP_*` family — dialog and UI helpers: (`G_WHIP_MSG()`, `G_WHIP_YESNO()`, + `G_WHIP_MENU()`, `G_WHIP_CHECKLIST()`, `G_WHIP_INPUTBOX()`, `G_WHIP_PASSWORD()`, + `G_WHIP_VIEWFILE()`). Prefer these for user interaction to maintain + consistent UX and behavior. Quick notes: + + - **Return value**: `$G_WHIP_RETURNED_VALUE` holds the helper result — a + single value for inputbox and menus, or an array of enabled indices for checklists. + - **Default item**: `$G_WHIP_DEFAULT_ITEM` sets the pre-selected value. For + `G_WHIP_MENU()` it must exactly match a menu label. + - **Checklist structure**: `$G_WHIP_CHECKLIST_ARRAY` entries are triples: + `'tag' 'Description' 'on/off'`. Use safe tags (letters, digits, underscore) + to avoid parsing issues. + - **Enumerated checklists**: set `G_WHIP_CHECKLIST_ENUM=1` to display numeric + indices in the UI to avoid long, complex, or non-safe keys. + - **Input validation**: `G_WHIP_INPUTBOX()` supports `$G_WHIP_INPUTBOX_REGEX` + and `$G_WHIP_INPUTBOX_REGEX_TEXT` to validate and describe allowed input. + The helper loops until input matches the regex or the user cancels (`|| return`). + - **Dialog sizing**: use `$G_WHIP_SIZE_X_MAX` to limit dialog width; helpers + respect terminal width and default to a max of 120 chars. ### Menu extension pattern (safe, minimal) @@ -159,7 +143,7 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. G_INIT ``` -- `G_WHIP_MENU` (single choice): +- `G_WHIP_MENU()` a menu of items the user can scroll through (choose one): ``` G_WHIP_MENU_ARRAY=( 'Start' 'Start the service' 'Stop' 'Stop the service' ) G_WHIP_DEFAULT_ITEM='Start' @@ -170,36 +154,37 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. esac ``` -- `$G_WHIP_CHECKLIST_ARRAY` (multi-select): +- `G_WHIP_CHECKLIST()` a list of items the user can enabled/disabled (multi-select): ``` G_WHIP_CHECKLIST_ARRAY=() G_WHIP_CHECKLIST_ARRAY+=( '5' 'Enable Foo' "${aENABLED[5]:=0}" ) G_WHIP_CHECKLIST_ARRAY+=( '6' 'Enable Bar' "${aENABLED[6]:=0}" ) + # Set G_WHIP_CHECKLIST_ENUM=1 to display numeric indices in the UI G_WHIP_CHECKLIST 'Choose features to enable:' || return for i in $G_WHIP_RETURNED_VALUE; do aENABLED[$i]=1; done Save > "$FP_SAVEFILE" ``` -- `G_WHIP_INPUTBOX` (validated input): +- `G_WHIP_INPUTBOX()` a text entry box (validated input): ``` G_WHIP_INPUTBOX_REGEX='^[0-9]+$' G_WHIP_INPUTBOX_REGEX_TEXT='a number' G_WHIP_DEFAULT_ITEM=10 G_WHIP_INPUTBOX 'Set retry count:' || return RETRIES=$G_WHIP_RETURNED_VALUE ``` -- `G_WHIP_YESNO` (confirmation): +- `G_WHIP_YESNO()` Yes/No prompt (confirmation): ``` if G_WHIP_YESNO 'Delete backup?'; then G_EXEC rm -rf "$TARGET" fi ``` -- `G_WHIP_VIEWFILE` (show a logfile): +- `G_WHIP_VIEWFILE()` displays a file that can be scrolled: ``` log=1 G_WHIP_VIEWFILE "$FP_LOG" || return ``` -- `G_TRUNCATE_MID` (shorten long strings by squishing the middle characters): +- `G_TRUNCATE_MID()` shorten long strings by squishing the middle characters: ``` G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26 # -> "Long text to... the middle" @@ -209,34 +194,35 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. # al...al # alphab # alpha - # ... and so on + # alph + # alp ``` -- `Save()` persistence pattern (follow dietpi-banner conventions): +- `Save()` example persistence pattern (beware of escape sequences): ``` Save(){ - # `echo` text to be evaluated when re-loading - # Call with `Save > "preference/filepath"` + # `echo` text to be eval-ed when re-loading + # Call with `Save > "preference/file/path"` echo "aDESCRIPTION[10]='${aDESCRIPTION[10]}'" for i in "${!aENABLED[@]}"; do echo "aENABLED[$i]=${aENABLED[$i]}"; done for i in {0..6}; do val="${aCOLOUR[$i]}"; esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g'); esc=${esc//\'/\\\'}; echo "aCOLOUR[$i]='$esc'"; done } ``` -### Banner extension pattern (example) +### Dietpi-banner extension pattern -When adding banner items (e.g. `dietpi-banner`), follow this minimal pattern: +When adding banner items, follow this minimal pattern: - Describe: add the label to `aDESCRIPTION[index]` and a default to - `aENABLED[index]` during initialization. + `aENABLED[index]` during initialization if relevant (the standard default is disabled=0). + If the item needs to show in the main menu checklist, add `index` to MENU_ITEMS. -- Output: implement `Print_()` or add a guarded line in `Print_Banner_raw()`: +- Output: implement `Get_()` and add a guarded line in `Print_Banner_raw()`: ``` - (( ${aENABLED[index]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[index]} $GREEN_SEPARATOR $(Print_)" + (( ${aENABLED[index]} )) && Print_Item_State "${aDESCRIPTION[index]}" "$(Get_Shortname 2>&1)" ``` -- Persist: ensure `Save()` writes `aENABLED[index]=...` and any custom - `aDESCRIPTION[...]` lines so the state survives restarts. +- Persist: ensure `Save()` writes `aENABLED[index]=...` so the state survives restarts. ### Nested / multi-page menu pattern diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index d61bac83a5..a690722d32 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -5,7 +5,7 @@ # #//////////////////////////////////// # Created by Daniel Knight / daniel.knight@dietpi.com / dietpi.com - # Updated May 2026 by Tim Olson / timjolson@users.noreply.github.com + # Updated July 2026 by Tim Olson / timjolson@users.noreply.github.com # #//////////////////////////////////// # From 06a431c8dc4469014774a5ebbf3c37a2b19ae63a Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 29 Jul 2026 18:43:16 -0500 Subject: [PATCH 57/69] fixed Disk Usage not working with word-wrap due to format codes being surrounded by whitespace sometimes --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a690722d32..8e02f409d6 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -890,7 +890,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" do IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" # Print aligned columns: {name} : {percent used} : {used} of {size} - printf "%b %b%-${length}s %b %b%6b %b %b%7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" + printf "%b %b%-${length}s %b%b %6b %b%b %7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" done } From 10fd9f69e3fe93cc17054481a8f1ffe79c8b8db9 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 30 Jul 2026 21:52:58 +0200 Subject: [PATCH 58/69] dietpi-banner: code alignments * "systemd" is explicitly written with a lower-case "s". It would be "okay" to use an upper-case "s" at the beginning of a sentence, but we keep strict lower-case everywhere else in DietPi texts as well. To quote the makers: "The only situation where we find it OK to use an uppercase letter in the name (but don't like it either) is if you start a sentence with systemd." * Use single-quotes for string, where no variable expension is intended, except in (the left-side of) bash conditionals "[[ ]]", where quotation has no effect. * newline after loop condition before "do", same like after if condition before "then" * Skip double-quotes when assigning a single variable or command substitution, as it is not needed in this case. Skip as well for "case" input pattern, where it is not needed either. * Replace some on-line if-then(-else) with && (||) * Skip "-n" and "-z" in bash conditionals, only use "!" to check for empty expressions. Also avoid redundant "${var:-}" to replace an empty variable with an empty string, redundant in modern shells. * Avoid muting STDERR where it is not expected (findmnt call), especially for testing/beta phase * Use "$" for variable calls in bash arithmetics as well. It is not needed, but makes it easier to distinguish between variable calls and assignments when reviewing a script, especially if editors do syntax highlighting, or when searching for those via grep. * Use %s for printf placeholders instead of %b, if no ANSI code expansion or similar is expected. * Use -v flag explicitly when "unset"ing variables. Very rare chance that the same name is used for a function, especially in a called (instead of sourced) script, but I started with this convention at some point. --- dietpi/func/dietpi-banner | 213 +++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 105 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 8e02f409d6..c727c705e6 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -66,7 +66,7 @@ [network_usage]='Network usage' [disk_usage]='Disk usage' [disk_patterns]='does not show in the menu. allows disk patterns to be enabled/disabled' - [systemd]='Systemd status' + [systemd]='systemd status' [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up @@ -171,24 +171,24 @@ done # Migrate legacy disk display options - if [[ -v aENABLED[disk_rootfs] ]] + if [[ -v 'aENABLED[disk_rootfs]' ]] then if (( aENABLED[disk_rootfs] )) then aDISK_SPACE_MOUNTS+=('/') aENABLED[disk_usage]=1 fi - unset "aENABLED[disk_rootfs]" + unset -v 'aENABLED[disk_rootfs]' DIETPI_BANNER_MIGRATED_DURING_RUN=1 fi - if [[ -v aENABLED[disk_userdata] ]] + if [[ -v 'aENABLED[disk_userdata]' ]] then if (( aENABLED[disk_userdata] )) then aDISK_SPACE_MOUNTS+=('/mnt/dietpi_userdata') aENABLED[disk_usage]=1 fi - unset "aENABLED[disk_userdata]" + unset -v 'aENABLED[disk_userdata]' DIETPI_BANNER_MIGRATED_DURING_RUN=1 fi @@ -336,9 +336,9 @@ # Skip a transient migrated command [[ $i == "$migrated_command_key" ]] && continue # Skip empty commands - [[ -n "${aCUSTOM_COMMANDS[$i]:-}" ]] || continue + [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue # Escape single quotes in the key and value for safe embedding - key=${i} + key=$i key=${key//\'/\\\'} cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} @@ -510,7 +510,7 @@ $CLI_LINE" # Use mawk for consistent floating-point formatting mawk -v v="$bytes" 'BEGIN{printf "%.1f", v/(1024*1024*1024)}' else - echo -e "???" + echo '???' fi } @@ -553,7 +553,7 @@ $CLI_LINE" for IFACE in "${observed_ifaces[@]}" do # strip kernel-style peer suffix (e.g. eth0@if8) and use base name - local ifname="${IFACE%%@*}" + local ifname=${IFACE%%@*} # skip loopback [[ $ifname == 'lo' ]] && continue @@ -628,11 +628,9 @@ $CLI_LINE" Menu_Disk_Mounts() { - # ignore check for single/double quote expansion issue; single/double quotes for glob; - # shellcheck disable=SC2016,SC2086 - pattern_hint="The matching mount 'target's returned by the 'findmnt' command will be reported. -To use regex, start the pattern with 're:', otherwise shell globbing is used. -For example: '/mnt/*' or 're:^/mnt/.*/*$'" + pattern_hint='The matching mount "target"s returned by the "findmnt" command will be reported. +To use regex, start the pattern with "re:", otherwise shell globbing is used. +For example: "/mnt/*" or "re:^/mnt/.*/*$"' # Build mount list from findmnt (preserve findmnt order) local -a available_mounts @@ -657,34 +655,36 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Add configured-but-missing mounts at the top. # Tags are created to be safe for G_WHIP_CHECKLIST (no spaces, no special characters). # The tag is used to map back to the mount path. - for p in "${aDISK_SPACE_MOUNTS[@]}"; do - [[ -n "$p" && -z ${avail_map[$p]} ]] || continue + for p in "${aDISK_SPACE_MOUNTS[@]}" + do + [[ $p && ! ${avail_map[$p]} ]] || continue tag="m$idx" desc="[not found] $p" default='on' G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") - tag2mount["$tag"]="$p" + tag2mount["$tag"]=$p ((idx++)) done # Add Custom matching option to allow adding user patterns. local custom_tag='CUSTOM_MATCHING' if (( ${aENABLED[disk_patterns]} )); then default='on'; else default='off'; fi - G_WHIP_CHECKLIST_ARRAY+=("$custom_tag" "Custom matching (globs/regex patterns)" "$default") + G_WHIP_CHECKLIST_ARRAY+=("$custom_tag" 'Custom matching (globs/regex patterns)' "$default") # Add available mounts from `findmnt` to the list. - for m in "${available_mounts[@]}"; do + for m in "${available_mounts[@]}" + do tag="m$idx" - desc="$m" - if [[ ${cfg_map[$m]} ]]; then default='on'; else default='off'; fi + desc=$m + [[ ${cfg_map[$m]} ]] && default='on' || default='off' G_WHIP_CHECKLIST_ARRAY+=("$tag" "$desc" "$default") - tag2mount["$tag"]="$m" + tag2mount["$tag"]=$m ((idx++)) done # Display checklist (enumerated) G_WHIP_CHECKLIST_ENUM=1 - G_WHIP_CHECKLIST "Select mounts to track. Use 'Custom matching' to manage globs/regex patterns." + G_WHIP_CHECKLIST 'Select mounts to track. Use "Custom matching" to manage globs/regex patterns.' local sel aENABLED[disk_patterns]=0 # Manage_Disk_Patterns sets to 1 if applicable @@ -692,9 +692,10 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Process selections: G_WHIP_RETURNED_VALUE contains selected tags. # Rebuild mounts array and call Manage_Disk_Patterns if the user selected the custom matching option. - for sel in $G_WHIP_RETURNED_VALUE; do - if [[ $sel == "$custom_tag" ]]; then Menu_Disk_Patterns; continue; fi - if [[ ${tag2mount[$sel]} ]]; then aDISK_SPACE_MOUNTS+=("${tag2mount[$sel]}"); fi + for sel in $G_WHIP_RETURNED_VALUE + do + [[ $sel == "$custom_tag" ]] && { Menu_Disk_Patterns; continue; } + [[ ${tag2mount[$sel]} ]] && aDISK_SPACE_MOUNTS+=("${tag2mount[$sel]}") done } @@ -704,15 +705,16 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" local pattern_hint sel idx # Multi-line help text for the patterns submenu - pattern_hint="To use regex, start the pattern with 're:', otherwise shell globbing is used. -For example: '/mnt/*' or 're:^/mnt/.*/*$'" + pattern_hint='To use regex, start the pattern with "re:", otherwise shell globbing is used. +For example: "/mnt/*" or "re:^/mnt/.*/*$"' while : do # Build menu: Add first, then each active pattern, then Done G_WHIP_MENU_ARRAY=() - G_WHIP_MENU_ARRAY+=( 'Add' 'Add a new pattern' ) - for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do + G_WHIP_MENU_ARRAY+=('Add' 'Add a new pattern') + for idx in "${!aDISK_SPACE_PATTERNS[@]}" + do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}") done G_WHIP_MENU_ARRAY+=( 'Done' 'Finish pattern configuration' ) @@ -720,26 +722,24 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" G_WHIP_MENU "Manage disk space match patterns. The matching mounts returned by 'findmnt' will be reported.\n$pattern_hint" || break sel=$G_WHIP_RETURNED_VALUE - case "$sel" in - Add) - G_WHIP_DEFAULT_ITEM='' + case $sel in + 'Add') + G_WHIP_DEFAULT_ITEM= G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a pattern G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' G_WHIP_INPUTBOX "$pattern_hint\n\nEdit pattern (empty removes it):" || continue aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") ;; - Done) - break - ;; + 'Done') break;; *) # Selected an existing pattern index -> edit in-place - if [[ $sel =~ ^[0-9]+$ && -n "${aDISK_SPACE_PATTERNS[$sel]:-}" ]] + if [[ $sel =~ ^[0-9]+$ && ${aDISK_SPACE_PATTERNS[$sel]} ]] then G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a pattern G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' G_WHIP_INPUTBOX "$pattern_hint\n\nEdit pattern (empty removes it):" - aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + aDISK_SPACE_PATTERNS[$sel]=$G_WHIP_RETURNED_VALUE else # Unknown selection, ignore continue @@ -747,9 +747,10 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" ;; esac - for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do + for idx in "${!aDISK_SPACE_PATTERNS[@]}" + do # Remove any empty entries (cleared by user) - [[ -n "${aDISK_SPACE_PATTERNS[$idx]:-}" ]] || unset 'aDISK_SPACE_PATTERNS[$idx]' + [[ ${aDISK_SPACE_PATTERNS[$idx]} ]] || unset -v 'aDISK_SPACE_PATTERNS[$idx]' done done @@ -778,17 +779,17 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Pull all mounts once (-D -n -r) in bytes (-b). Rebuild TARGET safely # (handles spaces) and compute USE% with one decimal using mawk from USED/SIZE. # Use ASCII Unit Separator (0x1F) as a safe field delimiter to avoid collisions with mount names - mapfile -t mount_lines < <(findmnt -b -Dnro TARGET,USED,SIZE --real 2>/dev/null | mawk -v SEP="$(printf '\037')" '{used=$(NF-1); size=$NF; target=$1; for(i=2;i<=NF-2;i++) target=target" "$i; perc=(size>0? used/size*100 : 0); printf "%s%s%s%s%s%s%s\n", target, SEP, used, SEP, size, SEP, sprintf("%.1f%%", perc)}') + mapfile -t mount_lines < <(findmnt -b -Dnro TARGET,USED,SIZE --real | mawk -v SEP="$(printf '\037')" '{used=$(NF-1); size=$NF; target=$1; for(i=2;i<=NF-2;i++) target=target" "$i; perc=(size>0? used/size*100 : 0); printf "%s%s%s%s%s%s%s\n", target, SEP, used, SEP, size, SEP, sprintf("%.1f%%", perc)}') # Keep mount_order[] to preserve findmnt's (consistent) order and aDISK_SPACE_PATTERNS' order. for line in "${mount_lines[@]}" do # Each line fields are separated by ASCII Unit Separator (0x1F): TARGETUSEDSIZEUSE% IFS=$'\x1f' read -r mnt_target used size perc <<< "$line" - mount_used["$mnt_target"]="$used" - mount_size["$mnt_target"]="$size" - mount_perc["$mnt_target"]="$perc" - mount_name["$mnt_target"]="$(basename "$mnt_target")" + mount_used["$mnt_target"]=$used + mount_size["$mnt_target"]=$size + mount_perc["$mnt_target"]=$perc + mount_name["$mnt_target"]=$(basename "$mnt_target") mount_order+=("$mnt_target") done @@ -800,11 +801,11 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Test against explicit exact-match patterns (highest precedence) for p in "${aDISK_SPACE_MOUNTS[@]}" do - [[ "$mnt_target" == "$p" ]] && { matched=1; break; } + [[ $mnt_target == "$p" ]] && { matched=1; break; } done # If not matched yet, test globs and regex patterns from match_patterns if enabled - if (( matched == 0 )) && (( aENABLED[disk_patterns] == 1 )) + if (( $matched == 0 && ${aENABLED[disk_patterns]} == 1 )) then for pattern in "${aDISK_SPACE_PATTERNS[@]}" do @@ -817,7 +818,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" fi else # shellcheck disable=SC2254 - case "$mnt_target" in + case $mnt_target in $pattern) matched=1; break;; *) :;; esac @@ -825,16 +826,16 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" done fi - if (( matched )) + if (( $matched )) then - used="${mount_used[$mnt_target]}" - size="${mount_size[$mnt_target]}" - perc="${mount_perc[$mnt_target]}" - name="${mount_name[$mnt_target]}" + used=${mount_used[$mnt_target]} + size=${mount_size[$mnt_target]} + perc=${mount_perc[$mnt_target]} + name=${mount_name[$mnt_target]} # Convert raw bytes to GiB (IEC) numeric strings for display - used_fmt="$(Bytes_To_GiB "$used")" - size_fmt="$(Bytes_To_GiB "$size")" + used_fmt=$(Bytes_To_GiB "$used") + size_fmt=$(Bytes_To_GiB "$size") # Mark seen and collect formatted result (fields separated by ASCII Unit Separator) seen[$mnt_target]=1 @@ -852,11 +853,11 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # at the bottom of the list. for p in "${aDISK_SPACE_MOUNTS[@]}" do - [[ -n "$p" && -z ${seen[$p]} ]] || continue - used_fmt="???" - size_fmt="???" - perc="???" - name="$(basename "$p")" + [[ $p && ! ${seen[$p]} ]] || continue + used_fmt='???' + size_fmt='???' + perc='???' + name=$(basename "$p") results+=("$p"$'\x1f'"$used_fmt"$'\x1f'"$size_fmt"$'\x1f'"$perc"$'\x1f'"$name") done @@ -867,7 +868,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" for entry in "${results[@]}" do IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" - (( name_count["$name"]++ )) + ((name_count["$name"]++)) done local length=0 @@ -877,11 +878,11 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" if (( name_count["$name"] > 1 )) then - dname="$(G_TRUNCATE_MID "$mnt_target" 30)" + dname=$(G_TRUNCATE_MID "$mnt_target" 30) else - dname="$(G_TRUNCATE_MID "$name" 30)" + dname=$(G_TRUNCATE_MID "$name" 30) fi - (( ${#dname} > length )) && length=${#dname} + (( ${#dname} > $length )) && length=${#dname} results[$i]="$mnt_target"$'\x1f'"$used"$'\x1f'"$size"$'\x1f'"$perc"$'\x1f'"$dname" done @@ -890,7 +891,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" do IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" # Print aligned columns: {name} : {percent used} : {used} of {size} - printf "%b %b%-${length}s %b%b %6b %b%b %7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" + printf "%b %b%-${length}s %b%b %6s %b%b %7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" done } @@ -912,32 +913,33 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Help text # shellcheck disable=SC2016 hint='Example:\necho -e "${aCOLOUR[good]}Hello World! :)"' - name_hint="Enter a name for the command. It must be unique, safe, and not empty." + name_hint='Enter a name for the command. It must be unique, safe, and not empty.' while : do # Build menu: `Add` first, then each active command, then `Done` G_WHIP_MENU_ARRAY=() - G_WHIP_MENU_ARRAY+=( 'Add' 'Add a new pattern' ) - for key in "${aCUSTOM_COMMANDS_ORDER[@]}"; do + G_WHIP_MENU_ARRAY+=('Add' 'Add a new pattern') + for key in "${aCUSTOM_COMMANDS_ORDER[@]}" + do G_WHIP_MENU_ARRAY+=("$key" "${aCUSTOM_COMMANDS[$key]}") done - G_WHIP_MENU_ARRAY+=( 'Done' 'Finish commands configuration' ) + G_WHIP_MENU_ARRAY+=('Done' 'Finish commands configuration') G_WHIP_DEFAULT_ITEM='Done' G_WHIP_MENU "Manage custom commands. Each command gets a name and a corresponding shell command.\n$hint" || break sel=$G_WHIP_RETURNED_VALUE - case "$sel" in - Add) + case $sel in + 'Add') # Add a new command G_WHIP_DEFAULT_ITEM='' G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a command G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' G_WHIP_INPUTBOX "$hint\n\nEdit command (empty removes it):" || continue - cmd="$G_WHIP_RETURNED_VALUE" + cmd=$G_WHIP_RETURNED_VALUE - if [[ -z "$cmd" ]] + if [[ ! $cmd ]] then # Empty command, do not add to array continue @@ -946,12 +948,12 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" G_WHIP_INPUTBOX_REGEX='.+' # Non-empty G_WHIP_INPUTBOX_REGEX_TEXT='must be unique, safe, and must not be empty' - while :; + while : do # New command entered, prompt for a name G_WHIP_INPUTBOX "$name_hint" || continue - key="$G_WHIP_RETURNED_VALUE" - if [[ -n "${aCUSTOM_COMMANDS[$key]:-}" ]] + key=$G_WHIP_RETURNED_VALUE + if [[ ${aCUSTOM_COMMANDS[$key]} ]] then # Key already exists, prompt again continue @@ -960,63 +962,63 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" break fi done - aCUSTOM_COMMANDS+=([$key]="$cmd") + aCUSTOM_COMMANDS+=([$key]=$cmd) aCUSTOM_COMMANDS_ORDER+=("$key") fi ;; # End adding of command - Done) - break - ;; + 'Done') break;; *) # Edit a command - if [[ -n "$sel" ]] + if [[ $sel ]] then - G_WHIP_DEFAULT_ITEM="${aCUSTOM_COMMANDS[$sel]}" + G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} G_WHIP_INPUTBOX_REGEX='.*' # Allow empty input to clear a command G_WHIP_INPUTBOX_REGEX_TEXT='may be empty (clear to remove entry)' G_WHIP_INPUTBOX "$hint\n\nEdit command (empty removes it):" - cmd="$G_WHIP_RETURNED_VALUE" + cmd=$G_WHIP_RETURNED_VALUE - if [[ -z "$cmd" ]] + if [[ ! $cmd ]] then # Empty command, remove from array - unset "aCUSTOM_COMMANDS[$sel]" + unset -v "aCUSTOM_COMMANDS[$sel]" # Re-index array - for i in "${!aCUSTOM_COMMANDS_ORDER[@]}"; do - [[ "${aCUSTOM_COMMANDS_ORDER[$i]}" == "$sel" ]] && unset 'aCUSTOM_COMMANDS_ORDER[$i]' + for i in "${!aCUSTOM_COMMANDS_ORDER[@]}" + do + [[ ${aCUSTOM_COMMANDS_ORDER[$i]} == "$sel" ]] && unset -v 'aCUSTOM_COMMANDS_ORDER[$i]' done aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") continue else # New command entered, prompt for a name - G_WHIP_DEFAULT_ITEM="$sel" + G_WHIP_DEFAULT_ITEM=$sel G_WHIP_INPUTBOX_REGEX='.+' # Non-empty G_WHIP_INPUTBOX_REGEX_TEXT='must be unique, safe, and must not be empty' while :; do G_WHIP_INPUTBOX "$name_hint" || continue - key="$G_WHIP_RETURNED_VALUE" + key=$G_WHIP_RETURNED_VALUE if [[ $key == "$sel" ]] then # Key unchanged, just update the command - aCUSTOM_COMMANDS[$key]="$cmd" + aCUSTOM_COMMANDS[$key]=$cmd break - elif [[ -n "${aCUSTOM_COMMANDS[$key]:-}" && "$key" != "$sel" ]] + elif [[ ${aCUSTOM_COMMANDS[$key]} && $key != "$sel" ]] then # Key changed to an existing key, prompt again continue else # Key is new and valid, accept it # Re-index array - for i in "${!aCUSTOM_COMMANDS_ORDER[@]}"; do - [[ "${aCUSTOM_COMMANDS_ORDER[$i]}" == "$sel" ]] && unset 'aCUSTOM_COMMANDS_ORDER[$i]' + for i in "${!aCUSTOM_COMMANDS_ORDER[@]}" + do + [[ ${aCUSTOM_COMMANDS_ORDER[$i]} == "$sel" ]] && unset -v 'aCUSTOM_COMMANDS_ORDER[$i]' done aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") - unset "aCUSTOM_COMMANDS[$sel]" + unset -v "aCUSTOM_COMMANDS[$sel]" # Add the new key and command - aCUSTOM_COMMANDS+=([$key]="$cmd") + aCUSTOM_COMMANDS+=([$key]=$cmd) aCUSTOM_COMMANDS_ORDER+=("$key") break fi @@ -1039,11 +1041,11 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" local key output status for key in "${aCUSTOM_COMMANDS_ORDER[@]}" do - [[ -n "${aCUSTOM_COMMANDS[$key]:-}" ]] || continue + [[ ${aCUSTOM_COMMANDS[$key]} ]] || continue # Capture output and exit code - output="$(eval "${aCUSTOM_COMMANDS[$key]}" 2>&1)" + output=$(eval "${aCUSTOM_COMMANDS[$key]}" 2>&1) status=$? - if (( status )) + if (( $status )) then Print_Item_State "$key" "${aCOLOUR[alert]}Command failed (code $status): $output${COLOUR_RESET}" else @@ -1101,7 +1103,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" { # Print a single line item with green bullet, description "$1", green separator and state "$2" # state should contain color and format codes as needed - local subtitle="$1" state="$2" + local subtitle=$1 state=$2 echo -e "$CLI_BULLET${aCOLOUR[strong]} $subtitle $CLI_SEPARATOR $state$COLOUR_RESET" } @@ -1160,7 +1162,8 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Show separator if any relevant follow-up sections or updates are present. # Most flags are numeric (0/1); AVAILABLE_UPDATE is a version string when present. - if [[ -n "$AVAILABLE_UPDATE" ]] || (( aENABLED[dietpi_commands] || aENABLED[motd] || aENABLED[credits] || LIVE_PATCHES || PACKAGE_COUNT || REBOOT_REQUIRED )); then + if [[ $AVAILABLE_UPDATE ]] || (( ${aENABLED[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[credits]} || $LIVE_PATCHES || $PACKAGE_COUNT || $REBOOT_REQUIRED )) + then echo -e "$CLI_LINE" fi @@ -1170,9 +1173,9 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Notify user if prefs or colors were migrated from legacy numeric indices during this run (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" - (( DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) && echo -e " ${aCOLOUR[highlight]}Your custom command from '.dietpi-banner_custom' needs to be migrated. + (( $DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) && echo -e " ${aCOLOUR[highlight]}Your custom command from '.dietpi-banner_custom' needs to be migrated. Custom commands are now created through 'dietpi-banner'. You can run - a script as a command by configuring 'bash \"/path/to/script\"' .$COLOUR_RESET" + a script as a command by configuring 'bash \"/path/to/script\"'.$COLOUR_RESET" } Print_Banner() @@ -1214,7 +1217,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Special actions for some items (like submenus) that require additional configuration case $selection in - 'custom_commands') + 'custom_commands') Menu_Custom_Commands # If no commands are configured, disable the custom commands option (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 @@ -1237,7 +1240,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" 'disk_usage') Menu_Disk_Mounts # If no disks are configured, disable the disk usage option - if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( aENABLED[disk_patterns] == 0 ) )) + if (( ${#aDISK_SPACE_PATTERNS[@]} == 0 || ${aENABLED[disk_patterns]} == 0 )) then (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 fi @@ -1246,7 +1249,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # No additional configuration needed for these options ;; *) - G_DIETPI-NOTIFY 1 "Unknown menu selection: $selection" >&2 + G_DIETPI-NOTIFY 1 "Unknown menu selection: $selection" ;; esac done From ac0210631ac84eec2bb974415e57b9f9606e3cd5 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 30 Jul 2026 22:40:42 +0200 Subject: [PATCH 59/69] dietpi-banner: another code and output alignment * Always print closing CLI_LINE, as it was done before. The multi-line blocks with subheader print a leading CLI_LINE but no trailing one. * Restore empty line before credits, update notifications, dietpi-* commands, but remove additional newline after banner (the migration messages will be removed, all done during dietpi-update). If a color reset is missing somewhere, we should fix it in the respective Print_* command, but so far I couldn't find any, especially after Print_Custom_Commands uses Print_Item_State as well. --- dietpi/func/dietpi-banner | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c727c705e6..18dee11d30 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -131,12 +131,17 @@ # Runtime order of custom command keys (stores user-defined order for display, saving, and menu). aCUSTOM_COMMANDS_ORDER=() - if [[ -f $FP_SAVEFILE ]]; then - while IFS= read -r line; do + if [[ -f $FP_SAVEFILE ]] + then + while IFS= read -r line + do # match patterns like: aCUSTOM_COMMANDS['key']='value' OR aCUSTOM_COMMANDS[0]='value' - if [[ $line =~ ^aCUSTOM_COMMANDS\[\'([^\']+)\'\]= ]]; then + if [[ $line =~ ^aCUSTOM_COMMANDS\[\'([^\']+)\'\]= ]] + then aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") - elif [[ $line =~ ^aCUSTOM_COMMANDS\[([0-9]+)\]= ]]; then + + elif [[ $line =~ ^aCUSTOM_COMMANDS\[([0-9]+)\]= ]] + then aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") fi done < "$FP_SAVEFILE" @@ -1153,23 +1158,19 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[motd]}" "$motd" fi - # Network usage by namespace - (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 - # Disk usage - (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage 2>&1 - # Custom commands + # Multi-line blocks with separate CLI_LINE and header + # - Network usage by namespace + (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage + # - Disk usage + (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage + # - Custom commands (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands - - # Show separator if any relevant follow-up sections or updates are present. - # Most flags are numeric (0/1); AVAILABLE_UPDATE is a version string when present. - if [[ $AVAILABLE_UPDATE ]] || (( ${aENABLED[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[credits]} || $LIVE_PATCHES || $PACKAGE_COUNT || $REBOOT_REQUIRED )) - then - echo -e "$CLI_LINE" - fi + # Final closing CLI_LINE and empty line before credits, update notifications, dietpi-* commands + echo -e "$CLI_LINE\n" (( ${aENABLED[credits]} )) && Print_Credits - (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands Print_Updates + (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors were migrated from legacy numeric indices during this run (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" @@ -1189,7 +1190,6 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' else Print_Banner_raw fi - echo -e "$COLOUR_RESET" } Menu_Main() From 7722cb64e5286e61a081782a35848b97cb72dcf7 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 30 Jul 2026 22:55:51 +0200 Subject: [PATCH 60/69] dietpi-banner: use default color for some infos where currently "weak" is used The dark gray really is dark. For info which was explicitly selected by users, use the default white/bright grey, like all of the upper entry values do. This is already different from the "highlight" and "alert" colors used for all other output states where "weak" was used, so this does not cause overlaps. Keep "weak" color for the credits only as before. If we want to use it more widely in the banner, IMO never for keys or values, only for separation/ordering symbols or such things. --- dietpi/func/dietpi-banner | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 18dee11d30..3c29425cf3 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -433,7 +433,7 @@ $CLI_LINE" 0) # Get_Cert_Status_Raw executed, process output case $stat in - 'Valid until'*) stat="${aCOLOUR[weak]}$stat";; # valid cert, print line with weak color + 'Valid until'*) :;; # valid cert, print line with default color 'No certificate found') stat="${aCOLOUR[highlight]}$stat";; # highlight if there is no cert *) stat="${aCOLOUR[alert]}$stat";; # alert if cert expired or there was another error esac @@ -453,7 +453,7 @@ $CLI_LINE" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] then - echo "${aCOLOUR[weak]}No services failed" + echo 'No services failed' else echo "${aCOLOUR[alert]}$FAILED_CNT service(s) failed" fi @@ -495,7 +495,7 @@ $CLI_LINE" state="${aCOLOUR[alert]}$count_banned_ips IP(s) banned" else # low number of bans - state="${aCOLOUR[weak]}$count_banned_ips IP(s) banned" + state="$count_banned_ips IP(s) banned" fi ;; 1) state="${aCOLOUR[alert]}fail2ban-client not available";; @@ -597,7 +597,7 @@ $CLI_LINE" [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - printf '%b %b%-23s %b %bTX=%11s RX=%11s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" + printf '%b %-23s %b TX=%11s RX=%11s\n' "$CLI_BULLET" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "$TX" "$RX" done } @@ -896,7 +896,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' do IFS=$'\x1f' read -r mnt_target used size perc name <<< "$entry" # Print aligned columns: {name} : {percent used} : {used} of {size} - printf "%b %b%-${length}s %b%b %6s %b%b %7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used" "$size" + printf "%b %b%-${length}s %b%b %6s %b %7s GiB of %7s GiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "$used" "$size" done } @@ -904,7 +904,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' { local state=$(/boot/dietpi/dietpi-vpn status 2>&1) case $state in - Connected*) state="${aCOLOUR[weak]}$state${COLOUR_RESET}";; + Connected*) state="$state${COLOUR_RESET}";; Disconnected*) state="${aCOLOUR[alert]}$state${COLOUR_RESET}";; *) state="${aCOLOUR[highlight]}$state${COLOUR_RESET}";; esac @@ -1165,7 +1165,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage # - Custom commands (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands - # Final closing CLI_LINE and empty line before credits, update notifications, dietpi-* commands + # Final closing CLI_LINE and empty line before credits, update notifications, dietpi-* commands echo -e "$CLI_LINE\n" (( ${aENABLED[credits]} )) && Print_Credits From 7b71c6b25e970155e891ad650b6ed57b907bcfb1 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 30 Jul 2026 23:22:11 +0200 Subject: [PATCH 61/69] dietpi-banner: fix ASCII letter detection The ASCII V and W contain literal upper-case V characters. --- dietpi/func/dietpi-banner-wrap.awk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner-wrap.awk b/dietpi/func/dietpi-banner-wrap.awk index ce60a7870c..899a8e2eab 100644 --- a/dietpi/func/dietpi-banner-wrap.awk +++ b/dietpi/func/dietpi-banner-wrap.awk @@ -35,7 +35,7 @@ BEGIN { { ## ASCII ART: Skip or Hide - if (match($0, /^[^a-zA-Z0-9─]+$/)) { + if (match($0, /^[^a-zA-UX-Z0-9─]+$/)) { if (MAXCOL > (RSTART + RLENGTH)) {print $0} next } From 6a9cfa02910d90324659568b43184851c1718f08 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:11:52 +0200 Subject: [PATCH 62/69] dietpi-banner: add IPv6 support and simplify network usage function * IPv6 are not trivial to match selectively, due to the possible shortenings of 0000 hextets, even on both ends. * For Fail2Ban stats, it makes sense to match those widely, to not miss if e.g. the localhost itself got banned. The output syntax is luckily specific enough for this to still reliably only match the banned IPs. * "ip -br a" has simple output format as well, with the 1st IP being the 3rd argument. There is no need to filter this, it is either an IP or empty. * For matching public IPs, the regex for IPv6 can be narrowed down much, since only the 2000::/3 block is globally routable. * Merge Get_WAN_IP and printing the IP address line into Print_Data_Usage, to deduplicate the ns_cmd_prefix crafting. Sadly, the whole function cannot simply be called with the namespace prefix, since we'd need to add all used functions and variables into the subshell, too. It would require some reorder for this to be reasonable, e.g. to obtain only the raw values with a simple dash-compatible function, and craft the colored output lines afterwards in the parent function. But this can be done in a future PR. * Make some variables local, and omit or rename some others --- dietpi/func/dietpi-banner | 98 ++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 3c29425cf3..dca26aeaad 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -238,9 +238,6 @@ CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" CLI_SEPARATOR="${aCOLOUR[accent]}:$COLOUR_RESET" - # IP address detection (IPv4) - mawk-friendly (avoid + and {m,n}) - IP4_re='[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' - # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] @@ -465,11 +462,13 @@ $CLI_LINE" { if command -v fail2ban-client > /dev/null then + # IP address detection (very sloppy for IPv6) - avoid {m,n} to support mawk on Debian Bookworm + local IP_re='^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f:]*:[0-9a-f:]*)$' if [[ $EUID == 0 ]] then - fail2ban-client banned 2> /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /dev/null | mawk -F\' -v ip="$IP_re" '{for(i=2;i /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /dev/null | mawk -F\' -v ip="$IP_re" '{for(i=2;i ...) or the default namespace, and with sudo if needed + # Use an array prefix to run commands either in a netns, + # with sudo if needed, or the default namespace local ns_cmd_prefix=() if [[ $netns ]] then # add sudo if not root - (( $EUID )) && ns_cmd_prefix+=(sudo -n) + (( $EUID )) && ns_cmd_prefix=(sudo -n) # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi - # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -br link 2> /dev/null | mawk '{print $1}') + # Obtain public IP and GeoIP info + local ip=$("${ns_cmd_prefix[@]}" curl -sSfLm 1 'https://dietpi.com/geoip' 2>&1) - for IFACE in "${observed_ifaces[@]}" + # Match all IPv4 addresses, but IPv6 addresses only from globally routable 2000::/3 block + if [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|^[23][0-9a-f]{0,3}:[0-9a-f:]+ ]] + then + # Valid IP detected, keep as-is and print with namespace + echo -e "${aCOLOUR[alt]} $IP [$display_ns]" + + elif [[ ! $ip ]] + then + # If the IP is empty, it likely means the namespace is disconnected or has no Internet access. Print status message. + echo -e "${aCOLOUR[alert]} Disconnected $CLI_SEPARATOR ${aCOLOUR[strong]}$display_ns" + + else + # Print curl or sudo errors as is + echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" + fi + + # Query interface list once inside the (optional) namespace and match names - skip loopback + local ifaces iface + mapfile -t ifaces < <("${ns_cmd_prefix[@]}" ip -br link 2> /dev/null | mawk '$1!="lo" {print $1}') + for iface in "${ifaces[@]}" do # strip kernel-style peer suffix (e.g. eth0@if8) and use base name - local ifname=${IFACE%%@*} - # skip loopback - [[ $ifname == 'lo' ]] && continue + iface=${iface%%@*} - case $ifname in + case $iface in # wired / predictable names eth*|en*|lan*) :;; # wireless @@ -573,8 +575,8 @@ $CLI_LINE" esac local RX='N/A' TX='N/A' rx tx ip_addr=' - No IP - ' ip_addr_cidr - local rx_path="/sys/class/net/$ifname/statistics/rx_bytes" - local tx_path="/sys/class/net/$ifname/statistics/tx_bytes" + local rx_path="/sys/class/net/$iface/statistics/rx_bytes" + local tx_path="/sys/class/net/$iface/statistics/tx_bytes" if [[ $netns ]]; then @@ -591,42 +593,20 @@ $CLI_LINE" [[ $rx =~ ^[0-9]+$ ]] && RX="$(Bytes_To_GiB "$rx") GiB" [[ $tx =~ ^[0-9]+$ ]] && TX="$(Bytes_To_GiB "$tx") GiB" - # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". - # Scan all fields for a matching IPv4/CIDR token. - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9][0-9]*$" '{for(i=1;i<=NF;i++) if($i ~ re){print $i; exit}}') + # Get first IP address (3rd field) for this interface in this namespace. If none, leave "- No IP -". + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br addr show dev "$iface" scope global 2> /dev/null | mawk '$3!="" {print $3}}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - printf '%b %-23s %b TX=%11s RX=%11s\n' "$CLI_BULLET" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "$TX" "$RX" + printf '%b %-23s %b TX=%11s RX=%11s\n' "$CLI_BULLET" "$ip_addr ($iface)" "$CLI_SEPARATOR" "$TX" "$RX" done } + local namespace=() ns mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 {print $1}') # Prepend an empty entry so the default (root) namespace can be handled in the loop - namespaces=('' "${namespaces[@]}") - - for ns in "${namespaces[@]}" + for ns in '' "${namespaces[@]}" do - raw_ns=${ns:-default} - display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) - - IP=$(Get_WAN_IP "$ns" 2>&1) - - if [[ $IP =~ ^$IP4_re ]] - then - # Valid IP detected, keep as-is and print with namespace - echo -e "${aCOLOUR[alt]} $IP [$display_ns]" - - elif [[ ! $IP ]] - then - # If the IP is empty, it likely means the namespace is disconnected or has no Internet access. Print status message. - echo -e "${aCOLOUR[alert]} Disconnected $CLI_SEPARATOR ${aCOLOUR[strong]}$display_ns" - - else - # Print curl errors as is - echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" - fi - Print_Data_Usage "$ns" done } From d041c3d11b727f0ddf601715b14c8fcb00584125 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:19:42 +0200 Subject: [PATCH 63/69] dietpi-banner: fix variable names --- dietpi/func/dietpi-banner | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index dca26aeaad..1180b5aa8a 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -544,7 +544,7 @@ $CLI_LINE" if [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|^[23][0-9a-f]{0,3}:[0-9a-f:]+ ]] then # Valid IP detected, keep as-is and print with namespace - echo -e "${aCOLOUR[alt]} $IP [$display_ns]" + echo -e "${aCOLOUR[alt]} $ip [$display_ns]" elif [[ ! $ip ]] then @@ -553,7 +553,7 @@ $CLI_LINE" else # Print curl or sudo errors as is - echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" + echo -e "${aCOLOUR[highlight]} $ip [$display_ns]" fi # Query interface list once inside the (optional) namespace and match names - skip loopback @@ -602,7 +602,7 @@ $CLI_LINE" done } - local namespace=() ns + local namespaces=() ns mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 {print $1}') # Prepend an empty entry so the default (root) namespace can be handled in the loop for ns in '' "${namespaces[@]}" From cac2d7eb509ad824f022b328df89127e54d86ff3 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:29:06 +0200 Subject: [PATCH 64/69] dietpi-banner: little ASCII pattern fix W was accidentally excluded --- dietpi/func/dietpi-banner-wrap.awk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner-wrap.awk b/dietpi/func/dietpi-banner-wrap.awk index 899a8e2eab..56a1d5d82e 100644 --- a/dietpi/func/dietpi-banner-wrap.awk +++ b/dietpi/func/dietpi-banner-wrap.awk @@ -35,7 +35,7 @@ BEGIN { { ## ASCII ART: Skip or Hide - if (match($0, /^[^a-zA-UX-Z0-9─]+$/)) { + if (match($0, /^[^a-zA-UW-Z0-9─]+$/)) { if (MAXCOL > (RSTART + RLENGTH)) {print $0} next } From 644bb174de5dba0e43727623ae4a96cde8072c13 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:31:41 +0200 Subject: [PATCH 65/69] dietpi-banner: typo --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 1180b5aa8a..4d83aeb70e 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -594,7 +594,7 @@ $CLI_LINE" [[ $tx =~ ^[0-9]+$ ]] && TX="$(Bytes_To_GiB "$tx") GiB" # Get first IP address (3rd field) for this interface in this namespace. If none, leave "- No IP -". - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br addr show dev "$iface" scope global 2> /dev/null | mawk '$3!="" {print $3}}') + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br addr show dev "$iface" scope global 2> /dev/null | mawk '$3!="" {print $3}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue From caf97a4c2d4cebb8fd1434b4ef7aeb23325316e3 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:53:07 +0200 Subject: [PATCH 66/69] dietpi-banner: little simplification Make MENU_ITEMS array readonly, along with some other variables that are not intended to be change. Remove "unknown menu selection" error and "every other valid one" match instead. There is not really a way whiptail can return a value other than what MENU_ITEMS contains, and it makes maintainance/adding new items easier. If we'd really want to make a failsafe check for debugging purpose, we should instead check whether the returned value is in the MENU_ITEMS array. --- dietpi/func/dietpi-banner | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 4d83aeb70e..19b406ebac 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -70,7 +70,7 @@ [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up - MENU_ITEMS=( + readonly MENU_ITEMS=( large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban motd network_usage disk_usage custom_commands dietpi_commands credits word_wrap @@ -94,7 +94,7 @@ (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 # Default colours - COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) + readonly COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) # Colour and format references: # - `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` # - `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" @@ -234,9 +234,9 @@ # ---------------------------------------------------------------------------- # Derived convenience strings - CLI_LINE=" ${aCOLOUR[accent]}─────────────────────────────────────────────────────$COLOUR_RESET" - CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" - CLI_SEPARATOR="${aCOLOUR[accent]}:$COLOUR_RESET" + readonly CLI_LINE=" ${aCOLOUR[accent]}─────────────────────────────────────────────────────$COLOUR_RESET" + readonly CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" + readonly CLI_SEPARATOR="${aCOLOUR[accent]}:$COLOUR_RESET" # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" @@ -754,7 +754,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' if (( ( ${#aDISK_SPACE_PATTERNS[@]} == 0 ) || ( ${aENABLED[disk_patterns]} == 0 ) )) && (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) then echo -e "${aCOLOUR[highlight]}No disk patterns provided, nothing to display." - return + return 0 fi local -a mount_order=() mount_lines @@ -831,7 +831,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' if (( ${#results[@]} == 0 )) then echo -e "${aCOLOUR[highlight]}No mounts were matched, check your configuration." - return + return 0 fi # For explicit mounts configured but not present in findmnt, add placeholder entries @@ -1225,12 +1225,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' (( ${#aDISK_SPACE_MOUNTS[@]} == 0 )) && aENABLED[disk_usage]=0 fi ;; - dietpi_commands|credits|motd|network_usage|letsencrypt|systemd|fail2ban|large_hostname|device_model|uptime|kernel|cpu_temp|ram_usage|load_average|hostname|nis_domainname|lan_ip|wan_ip|vpn_status|weather) - # No additional configuration needed for these options - ;; - *) - G_DIETPI-NOTIFY 1 "Unknown menu selection: $selection" - ;; + *) :;; esac done From 7a1fbb60e8dc7832c89dbfd266b2380ed843509f Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:58:45 +0200 Subject: [PATCH 67/69] dietpi-banner: remove -D flag from findmnt It overrides "--real" and shows tmpfs and udev mountpoints that way, which are not commonly interesting for disk space info. --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 19b406ebac..e4dc7ebb49 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -619,7 +619,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' # Build mount list from findmnt (preserve findmnt order) local -a available_mounts - mapfile -t available_mounts < <(findmnt -Dnro TARGET --real 2>/dev/null) + mapfile -t available_mounts < <(findmnt -nro TARGET --real 2>/dev/null) # Build quick lookup maps used to determine which mounts are present # and which are configured by the user: # - `avail_map`: mounts currently detected on the system From e738639361e79183b873c97fcf3882debda80efe Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 31 Jul 2026 01:59:45 +0200 Subject: [PATCH 68/69] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1779274fdf..e9e8345743 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -209,7 +209,7 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. } ``` -### Dietpi-banner extension pattern +### DietPi-Banner extension pattern When adding banner items, follow this minimal pattern: From 549461aaf792b84494972db44482bd3f26cd7e81 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Mon, 3 Aug 2026 00:14:42 +0200 Subject: [PATCH 69/69] dietpi-banner: move migration to patches and fix single-quote escaping * This also removes the now obsolete aCUSTOM_COMMANDS_ORDER. * Single quotes `'` need to be escaped with `'\''` within single quotes. The backslash character is taken literally like everything else. So the single-quotation needs to be ended first, then an escaped single-quote character added outside of the quotation, then the single-quotation started again. An alternative would be `'"'"'`, to add the single-quote character within double quotes, instead of backslash-escaped. --- .update/patches | 44 +++++++++ dietpi/func/dietpi-banner | 182 ++++---------------------------------- 2 files changed, 60 insertions(+), 166 deletions(-) diff --git a/.update/patches b/.update/patches index 4d3d256d44..24387792ec 100755 --- a/.update/patches +++ b/.update/patches @@ -2493,6 +2493,50 @@ Patch_10_6() # Assure "mount" is not autoremoved on Forky, just install/mark as manual on all versions, as it is required even for systemd mount units, but degraded to recommendation on Forky G_AGI mount + # dietpi-banner migrations + if [[ -f '/boot/dietpi/.dietpi-banner' ]] + then + local i mapping name command + + # Migrate legacy numeric-indexed aENABLED to named form, skipping disk usage (7 and 8) + mapping=( + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip '' '' + weather custom_commands dietpi_commands motd vpn_status large_hostname + credits letsencrypt ram_usage load_average word_wrap kernel + ) + for i in {0..6} {9..20} + do + G_EXEC sed --follow-symlinks -i "s/^[[:blank:]]*aENABLED\[$i\]=/aENABLED[${mapping[i]}]=/" /boot/dietpi/.dietpi-banner + done + + # Migrate legacy disk usage options + G_EXEC sed --follow-symlinks -i '\|^[[:blank:]]*aENABLED\[7\]=1|c\aDISK_SPACE_MOUNTS+=(/)' /boot/dietpi/.dietpi-banner + G_EXEC sed --follow-symlinks -i '\|^[[:blank:]]*aENABLED\[8\]=1|c\aDISK_SPACE_MOUNTS+=(/mnt/dietpi_userdata)' /boot/dietpi/.dietpi-banner + grep -q '^aDISK_SPACE_MOUNTS+=' /boot/dietpi/.dietpi-banner && G_CONFIG_INJECT 'aENABLED\[disk_usage\]=' 'aENABLED[disk_usage]=1' /boot/dietpi/.dietpi-banner + + # Migrate legacy custom command file to aCUSTOM_COMMANDS array if present + if [[ -f '/boot/dietpi/.dietpi-banner_custom' ]] + then + if grep -q '^aENABLED\[custom_commands\]=1' /boot/dietpi/.dietpi-banner + then + name=$(sed -n '/^[[:blank:]]*aDESCRIPTION\[10\]=/{s/^[^=]*=//p;q}' /boot/dietpi/.dietpi-banner) + read -r command < /boot/dietpi/.dietpi-banner_custom + GCI_PRESERVE=1 G_CONFIG_INJECT "aCUSTOM_COMMANDS\[${name:-migrated}\]=" "aCUSTOM_COMMANDS[${name:-migrated}]='${command//\'/\'\\\'\'}'" /boot/dietpi/.dietpi-banner + fi + G_EXEC rm /boot/dietpi/.dietpi-banner_custom + fi + + # Migrate legacy numeric-indexed aCOLOUR to named form + mapping=(accent strong weak alert) + for i in {0..3} + do + G_EXEC sed --follow-symlinks -i "s/^[[:blank:]]*aCOLOUR\[$i\]=/aCOLOUR[${mapping[i]}]=/" /boot/dietpi/.dietpi-banner + done + + # Remove left invalid entries + G_EXEC sed --follow-symlinks -Ei -e '/^[[:blank:]]*a(ENABLED|COLOUR|DESCRIPTION)\[[0-9]+\]=/d' /boot/dietpi/.dietpi-banner + fi + # Software updates, migrations and patches if [[ -f '/boot/dietpi/.installed' ]] then diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index e4dc7ebb49..e2bd7b2893 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -43,7 +43,7 @@ readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' # Use associative arrays keyed by short names derived from the description - declare -A aDESCRIPTION=( + declare -Ar aDESCRIPTION=( [device_model]='Device model' [uptime]='Uptime' [cpu_temp]='CPU temp' @@ -71,8 +71,8 @@ ) # Set the array order for the menu, items must be listed here to show up readonly MENU_ITEMS=( - large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname - lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban motd + large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname + nis_domainname lan_ip wan_ip vpn_status weather letsencrypt systemd fail2ban motd network_usage disk_usage custom_commands dietpi_commands credits word_wrap ) @@ -96,10 +96,10 @@ # Default colours readonly COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) # Colour and format references: - # - `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` - # - `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" - # - `https://www.ditig.com/256-colors-cheat-sheet` - # - `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` + # - https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + # - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html under section "Character Attributes (SGR)" + # - https://www.ditig.com/256-colors-cheat-sheet + # - https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters declare -A aCOLOUR=( [accent]='\e[38;5;154m' # DietPi green | Lines, bullets and separators [strong]='\e[1m' # Bold white | Main descriptions @@ -114,9 +114,7 @@ BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Set default custom patterns and commands. If the user has a legacy .dietpi-banner_custom file, - # it will be migrated into aCUSTOM_COMMANDS later. - # shellcheck disable=SC2016 + # Custom commands declare -A aCUSTOM_COMMANDS=() # Default disk mounts (explicit paths) and patterns (globs/regex) @@ -127,112 +125,6 @@ # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" - # Set aCUSTOM_COMMANDS_ORDER from the assignments in the prefs file. - # Runtime order of custom command keys (stores user-defined order for display, saving, and menu). - aCUSTOM_COMMANDS_ORDER=() - - if [[ -f $FP_SAVEFILE ]] - then - while IFS= read -r line - do - # match patterns like: aCUSTOM_COMMANDS['key']='value' OR aCUSTOM_COMMANDS[0]='value' - if [[ $line =~ ^aCUSTOM_COMMANDS\[\'([^\']+)\'\]= ]] - then - aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") - - elif [[ $line =~ ^aCUSTOM_COMMANDS\[([0-9]+)\]= ]] - then - aCUSTOM_COMMANDS_ORDER+=("${BASH_REMATCH[1]}") - fi - done < "$FP_SAVEFILE" - fi - - # ---------------------------------------------------------------------------- - # Legacy config migration helper - # ---------------------------------------------------------------------------- - # Migration helper function - DIETPI_BANNER_LEGACY_MIGRATION() - { - # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form - local k legacy_indexed_enabled_keys=( - device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata - weather custom_commands dietpi_commands motd vpn_status large_hostname credits letsencrypt - ram_usage load_average word_wrap kernel network_usage disk_usage systemd fail2ban - ) - for k in "${!aENABLED[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_enabled_keys if there is one - # shellcheck disable=SC2015 - [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue - - # Unset old numeric index - unset -v "aENABLED[$k]" - - # Mark that a migration from legacy numeric indices occurred during this run - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - done - - # Migrate legacy disk display options - if [[ -v 'aENABLED[disk_rootfs]' ]] - then - if (( aENABLED[disk_rootfs] )) - then - aDISK_SPACE_MOUNTS+=('/') - aENABLED[disk_usage]=1 - fi - unset -v 'aENABLED[disk_rootfs]' - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - fi - if [[ -v 'aENABLED[disk_userdata]' ]] - then - if (( aENABLED[disk_userdata] )) - then - aDISK_SPACE_MOUNTS+=('/mnt/dietpi_userdata') - aENABLED[disk_usage]=1 - fi - unset -v 'aENABLED[disk_userdata]' - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - fi - - # Migrate legacy custom command file to aCUSTOM_COMMANDS array if present - readonly FP_CUSTOM='/boot/dietpi/.dietpi-banner_custom' - if [[ -f $FP_CUSTOM ]] - then - migrated_command_key="migrated-$(date '+%Y-%m-%d_%H-%M-%S')" - # Add a custom command that runs the external file with bash - aCUSTOM_COMMANDS+=([$migrated_command_key]="bash \"$FP_CUSTOM\"") - aCUSTOM_COMMANDS_ORDER+=("$migrated_command_key") - DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=1 - fi - - # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form - local legacy_indexed_colour_keys=(accent strong weak alert good highlight alt) - for k in "${!aCOLOUR[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_colour_keys if there is one - # shellcheck disable=SC2015 - [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue - - # Unset old numeric index - unset -v "aCOLOUR[$k]" - - # Mark that a migration from legacy numeric indices occurred during this run - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - done - } - # Flag indicating whether a legacy numeric->associative config migration happened during this run - DIETPI_BANNER_MIGRATED_DURING_RUN=0 - DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND=0 - # Run migration - DIETPI_BANNER_LEGACY_MIGRATION - # ---------------------------------------------------------------------------- - # Derived convenience strings readonly CLI_LINE=" ${aCOLOUR[accent]}─────────────────────────────────────────────────────$COLOUR_RESET" readonly CLI_BULLET=" ${aCOLOUR[accent]}-$COLOUR_RESET" @@ -312,7 +204,7 @@ # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') # Escape single quotes for safe embedding - esc=${esc//\'/\\\'} + esc=${esc//\'/\'\\\'\'} echo "aCOLOUR[$i]='$esc'" done @@ -320,7 +212,7 @@ for i in "${!aDISK_SPACE_MOUNTS[@]}" do mount=${aDISK_SPACE_MOUNTS[$i]} - mount=${mount//\'/\\\'} + mount=${mount//\'/\'\\\'\'} echo "aDISK_SPACE_MOUNTS[$i]='$mount'" done @@ -328,25 +220,10 @@ for i in "${!aDISK_SPACE_PATTERNS[@]}" do patt=${aDISK_SPACE_PATTERNS[$i]} - patt=${patt//\'/\\\'} + patt=${patt//\'/\'\\\'\'} echo "aDISK_SPACE_PATTERNS[$i]='$patt'" done - # Persist aCUSTOM_COMMANDS (associative indices) in user-specified order - for i in "${aCUSTOM_COMMANDS_ORDER[@]}" - do - # Skip a transient migrated command - [[ $i == "$migrated_command_key" ]] && continue - # Skip empty commands - [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue - # Escape single quotes in the key and value for safe embedding - key=$i - key=${key//\'/\\\'} - cmd=${aCUSTOM_COMMANDS[$i]} - cmd=${cmd//\'/\\\'} - echo "aCUSTOM_COMMANDS['$key']='$cmd'" - done - # Persist BW indent settings echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" echo "BW_INDENT_FIXED=$BW_INDENT_FIXED" @@ -702,7 +579,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}") done - G_WHIP_MENU_ARRAY+=( 'Done' 'Finish pattern configuration' ) + G_WHIP_MENU_ARRAY+=('Done' 'Finish pattern configuration') G_WHIP_DEFAULT_ITEM='Done' G_WHIP_MENU "Manage disk space match patterns. The matching mounts returned by 'findmnt' will be reported.\n$pattern_hint" || break @@ -905,7 +782,7 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' # Build menu: `Add` first, then each active command, then `Done` G_WHIP_MENU_ARRAY=() G_WHIP_MENU_ARRAY+=('Add' 'Add a new pattern') - for key in "${aCUSTOM_COMMANDS_ORDER[@]}" + for key in "${!aCUSTOM_COMMANDS[@]}" do G_WHIP_MENU_ARRAY+=("$key" "${aCUSTOM_COMMANDS[$key]}") done @@ -948,7 +825,6 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' fi done aCUSTOM_COMMANDS+=([$key]=$cmd) - aCUSTOM_COMMANDS_ORDER+=("$key") fi ;; # End adding of command 'Done') break;; @@ -966,12 +842,6 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' then # Empty command, remove from array unset -v "aCUSTOM_COMMANDS[$sel]" - # Re-index array - for i in "${!aCUSTOM_COMMANDS_ORDER[@]}" - do - [[ ${aCUSTOM_COMMANDS_ORDER[$i]} == "$sel" ]] && unset -v 'aCUSTOM_COMMANDS_ORDER[$i]' - done - aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") continue else # New command entered, prompt for a name @@ -988,23 +858,17 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' # Key unchanged, just update the command aCUSTOM_COMMANDS[$key]=$cmd break + elif [[ ${aCUSTOM_COMMANDS[$key]} && $key != "$sel" ]] then # Key changed to an existing key, prompt again continue else # Key is new and valid, accept it - # Re-index array - for i in "${!aCUSTOM_COMMANDS_ORDER[@]}" - do - [[ ${aCUSTOM_COMMANDS_ORDER[$i]} == "$sel" ]] && unset -v 'aCUSTOM_COMMANDS_ORDER[$i]' - done - aCUSTOM_COMMANDS_ORDER=("${aCUSTOM_COMMANDS_ORDER[@]}") unset -v "aCUSTOM_COMMANDS[$sel]" # Add the new key and command aCUSTOM_COMMANDS+=([$key]=$cmd) - aCUSTOM_COMMANDS_ORDER+=("$key") break fi done @@ -1018,13 +882,13 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' done # End command menu loop # If any commands are configured, enable the custom_commands option - (( ${#aCUSTOM_COMMANDS_ORDER[@]} > 0 )) && aENABLED[custom_commands]=1 + (( ${#aCUSTOM_COMMANDS[@]} )) && aENABLED[custom_commands]=1 } Print_Custom_Commands() { local key output status - for key in "${aCUSTOM_COMMANDS_ORDER[@]}" + for key in "${!aCUSTOM_COMMANDS[@]}" do [[ ${aCUSTOM_COMMANDS[$key]} ]] || continue # Capture output and exit code @@ -1151,12 +1015,6 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' (( ${aENABLED[credits]} )) && Print_Credits Print_Updates (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands - - # Notify user if prefs or colors were migrated from legacy numeric indices during this run - (( $DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[highlight]}Your preferences need to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" - (( $DIETPI_BANNER_MIGRATED_CUSTOM_COMMAND )) && echo -e " ${aCOLOUR[highlight]}Your custom command from '.dietpi-banner_custom' needs to be migrated. - Custom commands are now created through 'dietpi-banner'. You can run - a script as a command by configuring 'bash \"/path/to/script\"'.$COLOUR_RESET" } Print_Banner() @@ -1229,14 +1087,6 @@ For example: "/mnt/*" or "re:^/mnt/.*/*$"' esac done - # Before saving, create a backup of the existing prefs if migrating. - # FP_SAVEFILE gets truncated when calling Save() below, so we need to backup first. - if [[ -f $FP_SAVEFILE && $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]] - then - local backup="$FP_SAVEFILE.bak-$(date -Iseconds)" - cp -a "$FP_SAVEFILE" "$backup" - fi - Save > "$FP_SAVEFILE" }