From a9f55945b84a515a862af16a3b62e506ac8e0175 Mon Sep 17 00:00:00 2001 From: fatalerrors Date: Tue, 13 Oct 2026 12:15:49 +0200 Subject: [PATCH] disp bug fix --- README.md | 4 +- doc/CHANGELOG.md | 7 ++ profile.d/disp.sh | 201 +++++++++++++++++++++++++++++----------------- 3 files changed, 138 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index b09e7ad..f994a5d 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ A bar-style prompt showing current time, execution time of the last command | `clean` | filefct | Erase backup files in given directories, optionally recursive | | `conf_dump` | conf | Display the profile configuration file; `-s/--section NAME` restricts output to one section; `-a/--all` also lists commented-out keys (documented defaults, skipped when the key is set); an optional pattern argument filters keys by substring match; honours `NO_COLOR` | | `conf_save` | conf | Save or update a key=value pair in a configuration section: `conf_save
`; replaces an existing key in place or appends it to the section (creating the section if absent); works through a symlinked file and keeps its permissions; the value must be single-line and the running shell is not updated (applies at next load) | -| `disp` | disp | Display formatted info / warning / error / debug messages; long messages are word-wrapped and continuation lines are indented to align with the message text | +| `disp` | disp | Display formatted info / warning / error / debug messages; long messages are word-wrapped and continuation lines are indented to align with the message text; honours `NO_COLOR`, `QUIET=1` (hides info messages) and `DEBUG` > 1 (shows debug messages); warnings, errors and debug go to stderr | | `dwl` | net | Download a URL using curl, wget, or fetch transparently; supports `-t ` / `--timeout ` to cap the transfer time | | `expandlist` | filefct | Expand glob expressions into a quoted, separated list | | `fake_compile` | fun | Simulate a long compilation process with random warnings and errors | @@ -121,7 +121,7 @@ A bar-style prompt showing current time, execution time of the last command | `ku` | processes | Kill all processes owned by the given user name or ID | | `matrix` | rain | Console screensaver with Matrix-style digital rain (binary, kana, ascii charset) | | `mcd` | filefct | Create a directory and immediately move into it | -| `mdcat` | disp | Render Markdown files in terminal with colors, inline formatting, and framed code blocks | +| `mdcat` | disp | Render Markdown files (or stdin) in terminal with colors, inline formatting, tables and framed code blocks; honours `NO_COLOR` | | `meteo` | info | Display weather forecast for the configured or given city | | `myextip` | net | Get information about your public IP address | | `get_pkgmgr` | packages | Detect the active package manager of the running distribution (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`) | diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 58540cd..80f88d3 100755 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -46,6 +46,13 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`). - 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`. +- `disp`: a message hidden by `QUIET`/`DEBUG` no longer returns a non-zero + status; no reset escape sequence under `NO_COLOR`; debug messages go to + stderr; works under `set -u`; faster wrapping. +- `mdcat`: tables inside fenced code blocks are shown as code; code spans and + `\_`-style escapes are protected from emphasis/link parsing; `NO_COLOR` is + honoured in tables and headings; fewer subprocesses when rendering tables; + newlines in arguments no longer break option parsing. - `compress.sh`: option flags and loop variables no longer leak into the interactive shell. diff --git a/profile.d/disp.sh b/profile.d/disp.sh index 81238b6..907a0c6 100644 --- a/profile.d/disp.sh +++ b/profile.d/disp.sh @@ -124,10 +124,23 @@ export -f set_colors # Display a message # Usage: disp # Types: -# I : info (green) -# W : warning (yellow) -# E : error (red) -# D : debug (cyan) +# I : info (green), stdout +# W : warning (yellow), stderr +# E : error (red), stderr +# D : debug (cyan), stderr, shown only when DEBUG is an integer > 1 +# Without a recognised type (case-insensitive), the arguments are the message +# itself, printed unprefixed on stdout like an info. +# +# Environment: +# NO_COLOR non-empty: no colour or reset sequence is emitted +# QUIET=1 hides info and unprefixed messages (warnings/errors remain) +# COLUMNS terminal width used for wrapping (tput cols otherwise, 80 fallback) +# +# The message is wrapped to the terminal width on spaces, ANSI sequences count +# for no width, and continuation lines are aligned after the prefix. Backslash +# escapes (\t, \e, ...) are interpreted when printing (printf %b), but wrapping +# is computed before that, so a literal "\t" counts as two characters. +# A message hidden by QUIET/DEBUG still returns 0. disp() { local _disp_print_wrapped @@ -167,13 +180,18 @@ disp() local wrapped="" line="" line_vis=0 last_space=-1 local _i=0 _n=${#message} while (( _i < _n )); do - local _sub=${message:_i} - if [[ $_sub =~ ^($ansi_re) ]]; then - line+=${BASH_REMATCH[0]} - _i=$(( _i + ${#BASH_REMATCH[0]} )) - continue - fi local _c=${message:_i:1} + # Only a real ESC or a literal backslash can start an ANSI sequence; + # testing this first avoids copying the message tail (and running + # the regex) for every ordinary character. + if [[ $_c == $'\e' || $_c == '\' ]]; then + local _sub=${message:_i} + if [[ $_sub =~ ^($ansi_re) ]]; then + line+=${BASH_REMATCH[0]} + _i=$(( _i + ${#BASH_REMATCH[0]} )) + continue + fi + fi _i=$(( _i + 1 )) line+=$_c (( ++line_vis )) @@ -196,38 +214,37 @@ disp() done wrapped+=$line + # RESETCOL closes any colour left open by the message. It is skipped + # under NO_COLOR so that no escape sequence at all is emitted. + local reset="${RESETCOL:-}" + [[ -n ${NO_COLOR:-} ]] && reset="" + + # >&"$target_fd" sends the output to stdout (1) or stderr (2). The + # first line carries the prefix, continuation lines are indented to + # line up with the message body. local first_line=1 local line while IFS= read -r line || [[ -n "$line" ]]; do if (( first_line )); then if [[ -n "$prefix" ]]; then - if [[ "$target_fd" -eq 2 ]]; then - printf "%b\n" "${prefix} ${line}${RESETCOL}" >&2 - else - printf "%b\n" "${prefix} ${line}${RESETCOL}" - fi + printf "%b\n" "${prefix} ${line}${reset}" >&"$target_fd" else - if [[ "$target_fd" -eq 2 ]]; then - printf "%b\n" "${line}${RESETCOL}" >&2 - else - printf "%b\n" "${line}${RESETCOL}" - fi + printf "%b\n" "${line}${reset}" >&"$target_fd" fi first_line=0 else - if [[ "$target_fd" -eq 2 ]]; then - printf "%*s%b\n" "$indent_len" "" "${line}${RESETCOL}" >&2 - else - printf "%*s%b\n" "$indent_len" "" "${line}${RESETCOL}" - fi + printf "%*s%b\n" "$indent_len" "" "${line}${reset}" >&"$target_fd" fi done <<< "$wrapped" } # Handle NO_COLOR: disable colors if set local color_enabled=1 - [[ -n $NO_COLOR ]] && color_enabled=0 + [[ -n ${NO_COLOR:-} ]] && color_enabled=0 + # Each branch ends with an "if" (never a bare "[[ ]] && cmd") so that a + # message hidden by QUIET/DEBUG still returns 0: a non-zero status would + # trip "set -e", ERR traps, and "disp ... && next" chains. case ${1^^} in "I") if [[ $color_enabled -eq 1 ]]; then @@ -236,8 +253,9 @@ disp() local heads="[ info ]" fi shift - [[ -z $QUIET || $QUIET -ne 1 ]] && \ + if [[ ${QUIET:-} != 1 ]]; then _disp_print_wrapped "$heads" 1 "$*" + fi ;; "W") if [[ $color_enabled -eq 1 ]]; then @@ -264,12 +282,17 @@ disp() local heads="[ debug ]" fi shift - [[ -n $DEBUG && $DEBUG -gt 1 ]] && \ - _disp_print_wrapped "$heads" 1 "$*" + # Shown only when DEBUG is an integer greater than 1; sent to stderr + # like the other diagnostics, so it never pollutes captured output. + if [[ ${DEBUG:-} =~ ^[0-9]+$ && $DEBUG -gt 1 ]]; then + _disp_print_wrapped "$heads" 2 "$*" + fi ;; * ) - [[ -z $QUIET || $QUIET -ne 1 ]] && \ + # No (known) type: the whole argument list is the message, as for info + if [[ ${QUIET:-} != 1 ]]; then _disp_print_wrapped "" 1 "$*" + fi ;; esac } @@ -298,8 +321,29 @@ mdcat() style_off="$RESETCOL" fi - # Apply inline transforms in a safe order: code, links, then emphasis. - # This prevents emphasis parsing inside code spans or link URLs. + # Inline transforms. Code spans and backslash-escaped punctuation + # (\<, \>, \_, \*, ...) are lifted out first and replaced by numbered + # placeholders (\x01N\x02 and \x03N\x04), so that nothing inside them is + # reinterpreted (links, emphasis); they are put back, code spans styled, + # at the very end. Then links, bare URLs and emphasis. + local -a spans=() escs=() + local ph_open=$'\x01' ph_close=$'\x02' esc_open=$'\x03' esc_close=$'\x04' + while [[ "$text" =~ \`([^\`]+)\` ]]; do + local match="${BASH_REMATCH[0]}" + local before="${text%%"$match"*}" + local after="${text#*"$match"}" + spans+=("${BASH_REMATCH[1]}") + text="${before}${ph_open}$(( ${#spans[@]} - 1 ))${ph_close}${after}" + done + while [[ "$text" =~ \\([[:punct:]]) ]]; do + local match="${BASH_REMATCH[0]}" + local before="${text%%"$match"*}" + local after="${text#*"$match"}" + escs+=("${BASH_REMATCH[1]}") + text="${before}${esc_open}$(( ${#escs[@]} - 1 ))${esc_close}${after}" + done + + # Links: [label](url) while [[ "$text" =~ \[([^][]+)\]\(([^()]+)\) ]]; do local match="${BASH_REMATCH[0]}" local label="${BASH_REMATCH[1]}" @@ -333,14 +377,6 @@ mdcat() done fi - while [[ "$text" =~ \`([^\`]+)\` ]]; do - local match="${BASH_REMATCH[0]}" - local val="${BASH_REMATCH[1]}" - local before="${text%%"$match"*}" - local after="${text#*"$match"}" - text="${before}${code_on}${val}${style_off}${after}" - done - while [[ "$text" =~ \*\*([^*]+)\*\* ]]; do local match="${BASH_REMATCH[0]}" local val="${BASH_REMATCH[1]}" @@ -376,13 +412,14 @@ mdcat() text="${before}${pre}${italic_on}${val}${style_off}${post}${after}" done - # Unescape Markdown punctuation escapes, such as \<, \>, \_, and \*. - while [[ "$text" =~ \\([[:punct:]]) ]]; do - local match="${BASH_REMATCH[0]}" - local val="${BASH_REMATCH[1]}" - local before="${text%%"$match"*}" - local after="${text#*"$match"}" - text="${before}${val}${after}" + # Put the styled code spans and the escaped characters back (see the + # placeholders above). + local idx + for idx in "${!spans[@]}"; do + text="${text/"${ph_open}${idx}${ph_close}"/"${code_on}${spans[idx]}${style_off}"}" + done + for idx in "${!escs[@]}"; do + text="${text/"${esc_open}${idx}${esc_close}"/"${escs[idx]}"}" done printf "%s\n" "$text" @@ -471,11 +508,16 @@ mdcat() _mdcat_print_table() { local _mdcat_parse_table_row + # Split one table row into cells, joined with $sep (set by the caller, + # reached through dynamic scoping). Cells are separated by '|' only; an + # escaped "\|" is not supported. Trimming uses parameter expansion + # (strip leading, then trailing, whitespace) to avoid forking sed. _mdcat_parse_table_row() { - local input="$1" - local line - line=$(printf '%s' "$input" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//') + local input="$1" j + local line="$input" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" line="${line#|}" line="${line%|}" @@ -490,7 +532,8 @@ mdcat() cell="$rest" rest="" fi - cell=$(printf '%s' "$cell" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//') + cell="${cell#"${cell%%[![:space:]]*}"}" + cell="${cell%"${cell##*[![:space:]]}"}" cells+=("$cell") [[ -z "$rest" ]] && break done @@ -530,7 +573,12 @@ mdcat() for ((j=0; j<${#row[@]}; ++j)); do local styled visible key="$orow:$j" styled=$(_mdcat_style_inline "${row[j]}") - visible=$(printf '%b' "$styled" | sed -E 's/\x1B\[[0-9;]*[mK]//g') + # Visible width: expand the literal \e sequences, then strip + # every ANSI CSI sequence (no sed fork per cell). + printf -v visible '%b' "$styled" + while [[ $visible =~ $'\e'\[[0-9\;]*[A-Za-z] ]]; do + visible=${visible/"${BASH_REMATCH[0]}"/} + done cell_styled["$key"]="$styled" cell_vislen["$key"]=${#visible} [[ -z "${col_widths[j]}" ]] && col_widths[j]=0 @@ -544,47 +592,54 @@ mdcat() [[ -z "${col_widths[j]}" ]] && col_widths[j]=0 done - # Draw top border - local border="+" + # Colours are dropped under NO_COLOR, like everywhere else in mdcat. + local c_frame="$IBlack" c_head="$BBlue" c_off="$RESETCOL" c_cell="$RESETCOL" + if [[ -n ${NO_COLOR:-} ]]; then + c_frame="" c_head="" c_off="" c_cell="" + fi + + # Draw top border: one run of dashes per column (cell width + 2 spaces) + local border="+" seg for ((j=0; j