From f931db08084dd5d923505afa3cb8beef1b5ae89b Mon Sep 17 00:00:00 2001 From: fatalerrors Date: Tue, 13 Oct 2026 12:02:01 +0200 Subject: [PATCH] fix bugs --- README.md | 8 ++- doc/CHANGELOG.md | 10 ++++ doc/profile.conf.example | 6 ++- doc/profile.conf.fatalerrors | 7 ++- profile.d/debug.sh | 100 +++++++++++++++++++++++++++-------- profile.d/disp.sh | 2 +- profile.d/info.sh | 4 +- profile.d/net.sh | 2 +- profile.d/prompt.sh | 2 +- profile.d/pwd.sh | 4 +- profile.d/rain.sh | 6 +-- 11 files changed, 117 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 450e590..b09e7ad 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ A bar-style prompt showing current time, execution time of the last command | `rmspc` | filefct | Replace spaces in filenames with underscores (or a custom character) | | `setc` | lang | Set locale to standard C (POSIX) | | `setlocale` | lang | Set console locale to any installed locale | -| `settrace` | debug | Activate or deactivate ERR trap to display backtrace on script errors | +| `settrace` | debug | Activate (`--on`), deactivate (`--off`) or query (`--status`) an ERR trap that prints the exit status and a call stack (on stderr) for each failing command; `--force` replaces/removes an ERR trap set by something else; active at startup when `SETTRACE_DEFAULT_ENABLED=1` | | `showinfo` | info | Display welcome banner and system information (figlet + neofetch/fastfetch) | | `ssr` | ssh | SSH into a server as root, forwarding extra ssh options | | `taz` | compress | Compress files and directories into a chosen archive format | @@ -224,6 +224,12 @@ change the default without having to pass flags every time. | `UTAZ_DEFAULT_DIR_MODE` | `auto` | Host directory policy: `auto` (only when the archive has no single top-level directory), `always` or `never` | | `UTAZ_DEFAULT_IGNORE_CASE` | `0` | Set to `1` to match archive extensions case-insensitively (`.ZIP`, `.Tar.GZ`). Per call: `utaz -i` / `utaz -s` | +**`[debug]`** — `settrace` + +| Key | Default | Description | +| --- | --- | --- | +| `SETTRACE_DEFAULT_ENABLED` | `0` | Set to `1` to activate the error trace (`settrace --on`) at the startup of every interactive shell | + **`[filefct]`** — file utilities | Key | Default | Description | diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index cbc2a6b..58540cd 100755 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -14,6 +14,8 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`). - `utaz -i` / `--ignore-case` and `-s` / `--case-sensitive` switches to match archive extensions case-insensitively or not; the default is set by the new `UTAZ_DEFAULT_IGNORE_CASE` key of the `[compress]` section (default `0`). +- `SETTRACE_DEFAULT_ENABLED` key in the new `[debug]` configuration options + (default `0`): activates `settrace --on` at startup of interactive shells. - `utaz` supports bare `.zst` files and `.tzst` archives; `taz -q` is now effective. @@ -36,6 +38,14 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`). (it selected `xterm-direct` even inside tmux/screen or VTE terminals); multiplexers (tmux, screen) are now checked before emulator hints, since they inherit the emulator's variables. +- `debug.sh`: the trace is now written to stderr (it used to pollute captured + `$(...)` output) and includes the exit status; `backtrace` and `error` are + exported like `settrace`; `settrace` tells its own trap from a foreign one + (`--status`, and `--off` no longer removes a foreign trap without `--force`); + `--on` when already active is only a warning; `getopt` errors name the command. +- All modules: counters updated with `((x++))`-style commands (which return a + non-zero status when the value was 0) now use plain assignments, so they no + longer trigger the ERR trace or break `set -e`. - `compress.sh`: option flags and loop variables no longer leak into the interactive shell. diff --git a/doc/profile.conf.example b/doc/profile.conf.example index 31cf713..95303e2 100755 --- a/doc/profile.conf.example +++ b/doc/profile.conf.example @@ -56,7 +56,11 @@ PAGER=less # ============================================================================== [debug] -# (no configurable keys yet) +# Activate the error trace (settrace --on) at startup of interactive shells: +# every failing command then prints its exit status and a call stack to stderr. +# Verbose; meant for debugging scripts and the profile itself (0=no, 1=yes). +# Can be toggled at any time with settrace --on / --off. +#SETTRACE_DEFAULT_ENABLED=0 # ============================================================================== [disp] diff --git a/doc/profile.conf.fatalerrors b/doc/profile.conf.fatalerrors index 2615789..b33fbe3 100755 --- a/doc/profile.conf.fatalerrors +++ b/doc/profile.conf.fatalerrors @@ -42,7 +42,12 @@ TERM=xterm-256color #UTAZ_DEFAULT_IGNORE_CASE=0 [debug] -# Section used by debug.sh (nothing yet) +# Section used by debug.sh + +# Activate the error trace (settrace --on) at startup of interactive shells: +# every failing command then prints its exit status and a call stack to stderr +# (0=no, 1=yes). +#SETTRACE_DEFAULT_ENABLED=0 [disp] # Section used by disp.sh diff --git a/profile.d/debug.sh b/profile.d/debug.sh index 8b4e132..78921dd 100644 --- a/profile.d/debug.sh +++ b/profile.d/debug.sh @@ -37,48 +37,62 @@ load_conf "debug" # ------------------------------------------------------------------------------ -# Display a backtrace +# Display a backtrace of the current call stack (innermost frame first), one +# line per frame: function, file where it is defined, line of the call site. # Usage: backtrace backtrace() { printf "========= Call stack =========\n" local i=1 # We begin at 1 to ignore backtrace itself while [[ $i -lt ${#FUNCNAME[@]} ]]; do + # BASH_SOURCE[i] is the file defining FUNCNAME[i], and BASH_LINENO[i-1] + # the line of that file from which FUNCNAME[i-1] was called. printf '%15s() %s:%d\n' \ "${FUNCNAME[$i]}" "${BASH_SOURCE[$i]}" "${BASH_LINENO[$(( i-1 ))]}" - ((i++)) + i=$(( i + 1 )) done - unset i printf "==============================\n" } +export -f backtrace +# ------------------------------------------------------------------------------ + # ------------------------------------------------------------------------------ -# Function to be trapped for errors investigation +# Handler installed by settrace as ERR trap: report the failure and the call +# stack, then give back the exit status of the failed command so the trap does +# not alter the shell behaviour (e.g. under "set -e"). +# Everything goes to stderr: with errtrace a failure inside $(...) or a pipe +# would otherwise inject the trace into the captured output. +# Exported because an exported settrace may be used from child bash processes, +# where the trap needs this function to exist. error() { local errcode=$? - backtrace + printf "Command failed with exit status %d\n" "$errcode" >&2 + backtrace >&2 return $errcode } +export -f error +# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Activate or deactivate error trapping to display backtrace -# Usage: settrace <--on|--off|--status> +# Usage: settrace <--on|--off|--status> [--force] +# Startup behaviour is set by SETTRACE_DEFAULT_ENABLED in the [debug] section. settrace() { local PARSED - PARSED=$(getopt -oh --long help,on,off,status,force -- "$@") + PARSED=$(getopt -oh --long help,on,off,status,force -n settrace -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"settrace --help\" to display usage." return 1 fi - local -a _pargs=() - readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0') - set -- "${_pargs[@]}" + # getopt emits a shell-quoted, normalised argument list ("--" ends options) + eval set -- "$PARSED" # First pass: gather the requested action and flags. --on, --off and - # --status are mutually exclusive; --force only modifies --on. + # --status are mutually exclusive; --force only modifies --on and --off. local action="" force=0 while true; do case $1 in @@ -86,11 +100,14 @@ settrace() printf "Try to activate backtrace display for script debugging.\n\n" printf "Options:\n" printf "\t--on\t\tActivate backtrace generation\n" - printf "\t--force\t\tForce replacement of existing trap (use with --on)\n" + printf "\t--force\t\tReplace (with --on) or remove (with --off) an ERR trap\n" + printf "\t\t\tnot installed by settrace\n" printf "\t--off\t\tDeactivate backtrace generation\n" printf "\t--status\tDisplay whether the ERR trap is set\n\n" - printf "That function active a trap event on error. If the script you want to\n" - printf "debug overload the ERR bash trap, it will not work.\n" + printf "That function activates a trap event on error. If the script you want to\n" + printf "debug overloads the ERR bash trap, it will not work.\n\n" + printf "To activate it at every interactive startup, set SETTRACE_DEFAULT_ENABLED=1\n" + printf "in the [debug] section of the profile configuration.\n" return 0 ;; --on|--off|--status) @@ -117,25 +134,48 @@ settrace() done # Second pass: evaluate the current trap state, then apply the action. - local status="off" - [[ -n $(trap -p ERR) ]] && status="on" + # "trap -p ERR" prints the exact command line that re-creates the trap, so + # comparing it tells our own trap ("on") from one set by someone else + # ("foreign"); empty output means no trap at all ("off"). + # Limitation: bash hides the ERR trap from code running in a function (and + # its subshells) unless errtrace is set. Our own trap is always seen since + # settrace sets errtrace with it, but a foreign trap set without errtrace is + # reported as "off" (and can then be replaced without --force). + local status="off" current + current=$(trap -p ERR) + if [[ -n $current ]]; then + status="foreign" + [[ $current == "trap -- 'error' ERR" ]] && status="on" + fi case $action in on) - if [[ $status == "on" && $force -eq 0 ]]; then - disp E "ERR signal trap is already set. Use --force to replace it." + if [[ $status == "on" ]]; then + disp W "ERR signal trap is already set." + return 0 + fi + if [[ $status == "foreign" && $force -eq 0 ]]; then + disp E "Another ERR signal trap is already set. Use --force to replace it." return 1 fi + # errtrace makes functions and subshells inherit the ERR trap set -o errtrace - trap "error" ERR + trap 'error' ERR ;; off) - [[ $status != "on" ]] && disp W "ERR signal trap is already unset!" + if [[ $status == "foreign" && $force -eq 0 ]]; then + disp E "The ERR signal trap was not set by settrace. Use --force to remove it." + return 1 + fi + [[ $status == "off" ]] && disp W "ERR signal trap is already unset!" trap - ERR set +o errtrace ;; status) - disp I "Trap signal on error is ${status}." + case $status in + foreign) disp I "Trap signal on error is set, but not by settrace." ;; + *) disp I "Trap signal on error is ${status}." ;; + esac ;; "") disp E "No action specified, use \"settrace --help\" to display usage." @@ -147,4 +187,22 @@ export -f settrace # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# Optional trace activation at startup (SETTRACE_DEFAULT_ENABLED, default 0). +# Interactive shells only: this file is sourced by non-interactive shells too, +# where an ERR trap would add noise to scripts. [[ $- == *i* ]] is used because +# profile.sh sets INTERACTIVE only after the modules are loaded. +# disp is not defined yet (modules load alphabetically), so the trap is set only +# when none exists, a path that never calls disp. An existing trap is left alone. +# Written as "if" so the sourcing status stays 0 when nothing is done. +case "${SETTRACE_DEFAULT_ENABLED:-0}" in + 1|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]) + if [[ $- == *i* && -z $(trap -p ERR) ]]; then + settrace --on + fi + ;; +esac +# ------------------------------------------------------------------------------ + + # EOF diff --git a/profile.d/disp.sh b/profile.d/disp.sh index a14a0a2..81238b6 100644 --- a/profile.d/disp.sh +++ b/profile.d/disp.sh @@ -176,7 +176,7 @@ disp() local _c=${message:_i:1} _i=$(( _i + 1 )) line+=$_c - (( line_vis++ )) + (( ++line_vis )) [[ $_c == ' ' ]] && last_space=${#line} if (( line_vis > width )); then if (( last_space > 0 )); then diff --git a/profile.d/info.sh b/profile.d/info.sh index a16d4f0..9c480d2 100644 --- a/profile.d/info.sh +++ b/profile.d/info.sh @@ -234,7 +234,7 @@ pinfo() [[ "$cpu_model" == "unknown" ]] && cpu_model="${_cpu_line#*: }" ;; "processor"*) - (( cpu_threads++ )) + cpu_threads=$(( cpu_threads + 1 )) ;; "physical id"*) _cur_phys="${_cpu_line#*: }" @@ -269,7 +269,7 @@ pinfo() # --- Process count (glob over numeric /proc entries) --- local proc_count=0 _pdir for _pdir in /proc/[0-9]*/; do - [[ -d "$_pdir" ]] && (( proc_count++ )) + [[ -d "$_pdir" ]] && proc_count=$(( proc_count + 1 )) done # --- Shell and terminal --- diff --git a/profile.d/net.sh b/profile.d/net.sh index f36dbc8..918e6bc 100644 --- a/profile.d/net.sh +++ b/profile.d/net.sh @@ -295,7 +295,7 @@ isipv6() IFS=':' read -ra parts <<< "$seg" for part in "${parts[@]}"; do [[ $part =~ ^[0-9a-fA-F]{1,4}$ ]] || { ok=0; break; } - (( total++ )) + total=$(( total + 1 )) done (( ok )) || break done diff --git a/profile.d/prompt.sh b/profile.d/prompt.sh index a71e256..ff42e5d 100644 --- a/profile.d/prompt.sh +++ b/profile.d/prompt.sh @@ -118,7 +118,7 @@ load_theme() local _lth_line _lth_key _lth_value _lth_varname _lth_lineno=0 while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do - ((_lth_lineno++)) + _lth_lineno=$(( _lth_lineno + 1 )) _lth_line="${_lth_line%$'\r'}" # strip CR _lth_line="${_lth_line#"${_lth_line%%[![:space:]]*}"}" # ltrim _lth_line="${_lth_line%"${_lth_line##*[![:space:]]}"}" # rtrim diff --git a/profile.d/pwd.sh b/profile.d/pwd.sh index cd2cc74..483caad 100644 --- a/profile.d/pwd.sh +++ b/profile.d/pwd.sh @@ -234,7 +234,7 @@ genpwd() break fi - ((attempts++)) + attempts=$(( attempts + 1 )) done done @@ -256,7 +256,7 @@ genpwd() break fi - ((attempts++)) + attempts=$(( attempts + 1 )) done done diff --git a/profile.d/rain.sh b/profile.d/rain.sh index 0a1f4ca..7fec65a 100644 --- a/profile.d/rain.sh +++ b/profile.d/rain.sh @@ -300,7 +300,7 @@ _rain_engine() if ((100 * RANDOM / 32768 < falling_odd)); then if ((++rains[idx + 1] > term_height)); then rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata:num_rains*num_rain_metadata}") - ((num_rains--)) + num_rains=$(( num_rains - 1 )) continue fi fi @@ -359,7 +359,7 @@ _rain_engine() X=$((term_width * RANDOM / 32768 + 1)) Y=$((1 - drop_length)) rains=("${rains[@]}" "$X" "$Y" "$rain_drop" "$drop_color" "$drop_length") - ((num_rains++)) + num_rains=$(( num_rains + 1 )) fi do_render @@ -684,7 +684,7 @@ rainbow() for ((col = 0; col < term_width; col++)); do idx=$((col - offset)) while ((idx < 0)); do - ((idx += palette_width)) + idx=$(( idx + palette_width )) done idx=$((idx % palette_width)) row_line+="${RAINBOW_BG_PALETTE[idx]} "