From daa2ee755c1ffd13d2bc9b301c3c167067ff8cc0 Mon Sep 17 00:00:00 2001 From: fatalerrors Date: Tue, 13 Oct 2026 11:49:40 +0200 Subject: [PATCH] fix conf management --- README.md | 28 +++--- doc/CHANGELOG.md | 11 +++ doc/FAQ.md | 5 +- profile.d/conf.sh | 234 ++++++++++++++++++++++++++++++++-------------- 4 files changed, 192 insertions(+), 86 deletions(-) mode change 100755 => 100644 profile.d/conf.sh diff --git a/README.md b/README.md index 790be5d..450e590 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ A bar-style prompt showing current time, execution time of the last command | `busy` | fun | Monitor /dev/urandom for a hex pattern — look busy | | `check_updates` | updates | Check whether a newer profile version is available online; when called with `-q` at startup a 3-second network timeout is applied so a slow or absent network never delays the prompt | | `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; an optional pattern argument filters keys by substring match | -| `conf_save` | conf | Save or update a key=value pair in a configuration section: `conf_save
`; creates the section header automatically if absent | +| `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 | | `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 | @@ -188,21 +188,23 @@ automatically at startup: | *(absent or `smart`)* | Auto-detect the best available `terminfo` entry (default) | | any other string | Exported as-is — standard Unix behaviour | -When auto-detection runs, `COLORTERM=truecolor|24bit` is first evaluated as a -global truecolor flag. Each emulator hint is then checked in order; within each -branch a `*-direct` terminfo entry (24-bit colour) is tried before the -`*-256color` fallback. Only entries confirmed to exist via `terminfo` are used. +When auto-detection runs, `COLORTERM=truecolor|24bit` is evaluated as a global +truecolor flag: it does not pick an entry by itself, it only enables the +`*-direct` (24-bit colour) terminfo entries below, each of which falls back to its +`*-256color` counterpart. Hints are checked in the following order and the first +entry confirmed to exist via `terminfo` is used. Terminal multiplexers come first +because they inherit the outer emulator's variables (`TERM_PROGRAM`, +`VTE_VERSION`, …) but need their own `TERM`. -1. **`$COLORTERM`** — `truecolor` or `24bit` sets the truecolor flag for all subsequent probes -2. **`$TERM_PROGRAM`** — emulator name hints: +1. **`$TMUX`** — tmux session → `tmux-direct`¹ → `screen-direct`¹, then `tmux-256color`, then `screen-256color` +2. **`$STY`** — GNU screen session → `screen-direct`¹, then `screen-256color`, then `screen` +3. **`$TERM_PROGRAM`** — emulator name hints: - `iTerm.app` → `xterm-direct`¹, then `xterm-256color` - `WezTerm` → `xterm-direct`¹, then `wezterm`, then `xterm-256color` - `Hyper`, `vscode` → `xterm-direct`¹, then `xterm-256color` -3. **`$VTE_VERSION`** — GNOME Terminal, Tilix, … → `vte-direct`¹, then `vte-256color`, then `xterm-256color` -4. **`$WT_SESSION`** — Windows Terminal → `xterm-direct`¹, then `xterm-256color` -5. **`$TMUX`** — tmux session → `tmux-direct`¹ → `screen-direct`¹, then `tmux-256color`, then `screen-256color` -6. **`$STY`** — GNU screen session → `screen-direct`¹, then `screen-256color`, then `screen` -7. **Generic probe** — `xterm-256color` → `xterm-color` → `xterm` → `vt100` +4. **`$VTE_VERSION`** — GNOME Terminal, Tilix, … → `vte-direct`¹, then `vte-256color`, then `xterm-256color` +5. **`$WT_SESSION`** — Windows Terminal → `xterm-direct`¹, then `xterm-256color` +6. **Generic probe** — `xterm-256color` → `xterm-color` → `xterm` → `vt100` ¹ Only attempted when the truecolor flag is set (`COLORTERM=truecolor` or `24bit`). diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6113250..cbc2a6b 100755 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -25,6 +25,17 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`). fallback (no `pigz`) works again. - `utaz`: single top-level directory flattening, `--no-dir` target location, `foo.tar.gz` host directory name and `.tar.lz` extraction. +- `conf_save`: no longer replaces a symlinked configuration file by a regular + one, keeps the file permissions, rejects multi-line values, and inserts a new + key before the trailing comments/banner of its section instead of under the + next section's banner; leading blanks before a `[section]` are recognised. +- `conf_dump -a`: a commented default is no longer listed when the key is set + later in the same section; `NO_COLOR` is honoured; names with newlines or + backslashes in arguments are handled. +- `term_set`: `COLORTERM=truecolor` no longer short-circuits every other probe + (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. - `compress.sh`: option flags and loop variables no longer leak into the interactive shell. diff --git a/doc/FAQ.md b/doc/FAQ.md index 74f8084..fc35c03 100755 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -157,8 +157,9 @@ Set `TERM` in the `[system]` section of `profile.conf`: When `TERM` is absent or set to `smart`, `term_set` probes your terminal emulator's environment variables (`COLORTERM`, `TERM_PROGRAM`, `VTE_VERSION`, `WT_SESSION`, `TMUX`, `STY`) and then tests terminfo entries in preference -order. If you are experiencing display issues, run `term_set` interactively -and check the result with `echo $TERM`. See *Prompt & theming* below for +order (multiplexers first). If you are experiencing display issues, run +`TERM=smart; term_set` interactively (`term_set` leaves an already-set `TERM` +untouched) and check the result with `echo $TERM`. See *Prompt & theming* below for symptoms caused by a wrong `TERM` value. --- diff --git a/profile.d/conf.sh b/profile.d/conf.sh old mode 100755 new mode 100644 index c73760a..da719f8 --- a/profile.d/conf.sh +++ b/profile.d/conf.sh @@ -34,16 +34,44 @@ # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# Print the path of the configuration file the conf_* functions work on: +# the user file ($HOME/.profile.conf) when it exists, otherwise the installation +# file ($PROFILE_CONF, defaulting to $MYPATH/profile.conf). The returned path +# may not exist yet. +# Usage: _conf_path +_conf_path() +{ + if [[ -f "$HOME/.profile.conf" ]]; then + printf '%s\n' "$HOME/.profile.conf" + else + printf '%s\n' "${PROFILE_CONF:-${MYPATH}/profile.conf}" + fi +} +export -f _conf_path +# ------------------------------------------------------------------------------ + + # ------------------------------------------------------------------------------ # Save or update a key=value pair in a section of the profile configuration file. # The user configuration ($HOME/.profile.conf) is updated when it exists, # otherwise the installation configuration ($PROFILE_CONF or $MYPATH/profile.conf) # is used. The section header is created automatically when absent. # +# An existing key is replaced in place (duplicates in the same section are +# dropped); a new key is added at the end of the section's active lines, before +# its trailing blank/comment lines, so it never ends up under the banner of the +# next section. The running shell is NOT updated: the value is read again by +# parse_conf/load_conf the next time the profile is sourced. +# +# The value is stored verbatim, but parse_conf trims surrounding whitespace and +# quotes, drops backticks and "$(", and expands "$VAR" references when it reads +# the file back; values relying on those are therefore not preserved. +# # Usage: conf_save
# section : INI section name without brackets, e.g. "prompt" for [prompt] # key : variable name to set (alphanumeric and underscore only) -# value : value to assign (may be empty) +# value : value to assign (may be empty, must be a single line) conf_save() { if [[ $# -ne 3 ]]; then @@ -61,15 +89,21 @@ conf_save() disp E "conf_save: invalid key name '${key}' (alphanumeric and underscore only)." return 1 fi - - local conf_file - if [[ -f "$HOME/.profile.conf" ]]; then - conf_file="$HOME/.profile.conf" - else - conf_file="${PROFILE_CONF:-${MYPATH}/profile.conf}" + # A line break would split the entry and corrupt the file. + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then + disp E "conf_save: the value must not contain line breaks." + return 1 fi - local conf_dir="${conf_file%/*}" + local conf_file + conf_file=$(_conf_path) + # Follow a symlink (e.g. dotfiles manager) so the link itself is not + # replaced by a regular file when the temporary file is moved over it. + [[ -L "$conf_file" ]] && conf_file=$(readlink -f -- "$conf_file") + + # Directory part of the path; a bare file name lives in the current directory. + local conf_dir=. + [[ "$conf_file" == */* ]] && conf_dir="${conf_file%/*}" [[ -d "$conf_dir" ]] || mkdir -p "$conf_dir" || { disp E "conf_save: unable to create configuration directory: ${conf_dir}" return 1 @@ -86,26 +120,40 @@ conf_save() return 0 fi + # Temporary file next to the target so the final mv is an atomic rename. local tmp_file="${conf_file}.tmp.$$" # The value is passed via ENVIRON (not awk -v) so backslashes are preserved # verbatim; awk -v would interpret C escape sequences (\t, \n, \e, ...). + # awk state machine: in_sec is true while inside the target section, wrote + # once the key has been emitted, saw_sec once the section has been seen. _CONF_SAVE_VAL="$value" \ awk -v sec="$section" -v key="$key" ' + # Emit the held trailing blank/comment lines + function flush_pending( i) { + for (i = 1; i <= np; i++) print pend[i] + np = 0 + } + BEGIN { val = ENVIRON["_CONF_SAVE_VAL"] in_sec = 0 saw_sec = 0 wrote = 0 + np = 0 } { - if ($0 ~ /^\[[^]]+\][[:space:]]*$/) { + # Section header (leading blanks allowed, as parse_conf does) + if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) { + # Leaving the target section without having found the key: + # add it now, before the section trailing comments. if (in_sec && !wrote) { print key "=" val wrote = 1 } + flush_pending() - if ($0 ~ ("^\\[" sec "\\][[:space:]]*$")) { + if ($0 ~ ("^[[:space:]]*\\[" sec "\\][[:space:]]*$")) { in_sec = 1 saw_sec = 1 } else { @@ -116,6 +164,15 @@ conf_save() next } + # Until the key is written, hold blank and comment lines: if the + # section ends without the key, it must go before them. + if (in_sec && !wrote && $0 ~ /^[[:space:]]*([#;].*)?$/) { + pend[++np] = $0 + next + } + flush_pending() + + # Existing assignment of the key: replace the first, drop the others if (in_sec && $0 ~ ("^[[:space:]]*" key "[[:space:]]*=")) { if (!wrote) { print key "=" val @@ -130,9 +187,11 @@ conf_save() END { if (in_sec && !wrote) { print key "=" val + wrote = 1 } + flush_pending() if (!saw_sec) { - print "" + if (NR > 0) print "" print "[" sec "]" print key "=" val } @@ -143,7 +202,11 @@ conf_save() return 1 } - mv "$tmp_file" "$conf_file" || { + # mktemp-style default permissions must not replace the original ones + # (the file may hold private values); best effort, GNU chmod only. + chmod --reference="$conf_file" -- "$tmp_file" 2>/dev/null + + mv -- "$tmp_file" "$conf_file" || { rm -f "$tmp_file" disp E "conf_save: unable to replace configuration file: ${conf_file}" return 1 @@ -177,9 +240,8 @@ conf_dump() disp E "Invalid options, use \"conf_dump --help\" to display usage." return 1 fi - local -a _pargs=() - readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0') - set -- "${_pargs[@]}" + # Restore the getopt-normalised (shell-quoted) argument list, "--" ends options + eval set -- "$PARSED" while true; do case "$1" in @@ -219,28 +281,37 @@ conf_dump() [[ $# -gt 0 ]] && key_pattern="$1" local conf_file - if [[ -f "$HOME/.profile.conf" ]]; then - conf_file="$HOME/.profile.conf" - else - conf_file="${PROFILE_CONF:-${MYPATH}/profile.conf}" - fi + conf_file=$(_conf_path) if [[ ! -f "$conf_file" ]]; then disp E "conf_dump: configuration file not found: ${conf_file}" return 1 fi - # Colours are passed via ENVIRON to avoid awk -v escape interpretation. - _CONF_DUMP_SEC="${Blue:-}" \ - _CONF_DUMP_KEY_ACTIVE="${BGreen:-}" \ - _CONF_DUMP_KEY_DEFAULT="${BIWhite:-}" \ - _CONF_DUMP_RST="${RESETCOL:-}" \ - awk -v sec_filter="$section" -v key_filter="$key_pattern" -v show_all="$show_all" ' + # Honour NO_COLOR like disp does: empty colour strings give plain output. + local c_sec="${Blue:-}" c_active="${BGreen:-}" c_default="${BIWhite:-}" c_rst="${RESETCOL:-}" + if [[ -n ${NO_COLOR:-} ]]; then + c_sec="" c_active="" c_default="" c_rst="" + fi + + # Everything is passed via ENVIRON, not awk -v, to avoid awk interpreting + # backslash escapes (colours hold literal \e sequences; a filter could + # contain a backslash). The file is given twice: the first pass only + # collects the active keys, the second one prints. + _CONF_DUMP_SEC="$c_sec" \ + _CONF_DUMP_KEY_ACTIVE="$c_active" \ + _CONF_DUMP_KEY_DEFAULT="$c_default" \ + _CONF_DUMP_RST="$c_rst" \ + _CONF_DUMP_SECF="$section" \ + _CONF_DUMP_KEYF="$key_pattern" \ + awk -v show_all="$show_all" ' BEGIN { - c_sec = ENVIRON["_CONF_DUMP_SEC"] - c_active = ENVIRON["_CONF_DUMP_KEY_ACTIVE"] - c_default = ENVIRON["_CONF_DUMP_KEY_DEFAULT"] - c_rst = ENVIRON["_CONF_DUMP_RST"] + c_sec = ENVIRON["_CONF_DUMP_SEC"] + c_active = ENVIRON["_CONF_DUMP_KEY_ACTIVE"] + c_default = ENVIRON["_CONF_DUMP_KEY_DEFAULT"] + c_rst = ENVIRON["_CONF_DUMP_RST"] + sec_filter = ENVIRON["_CONF_DUMP_SECF"] + key_filter = ENVIRON["_CONF_DUMP_KEYF"] # Shell colour vars contain literal \e[…m strings; convert to # the actual ESC byte so awk print emits real ANSI sequences. gsub(/\\e/, "\033", c_sec) @@ -251,17 +322,35 @@ conf_dump() current_sec = "" hdr_printed = 0 found = 0 - # seen[sec:key] — tracks every key already printed to deduplicate - # commented entries and avoid re-showing an active key in white. + # active[sec:key] — keys set (uncommented) anywhere in the file, + # filled by the first pass. + # seen[sec:key] — keys already printed, to show each commented + # default at most once. + } + + # First pass (FNR == NR): record the active keys, print nothing. + # An active key suppresses its commented default wherever the latter + # appears in the section (before or after it). + FNR == NR { + line = $0; sub(/\r$/, "", line) + if (line ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) { + pass1_sec = line + sub(/^[[:space:]]*\[/, "", pass1_sec) + sub(/\][[:space:]]*$/, "", pass1_sec) + } else if (line ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) { + k = line; sub(/[[:space:]]*=.*$/, "", k); sub(/^[[:space:]]*/, "", k) + active[pass1_sec ":" k] = 1 + } + next } { sub(/\r$/, "") - # Section header - if ($0 ~ /^\[[^]]+\][[:space:]]*$/) { + # Section header (leading blanks allowed, as parse_conf does) + if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) { current_sec = $0 - sub(/^\[/, "", current_sec) + sub(/^[[:space:]]*\[/, "", current_sec) sub(/\][[:space:]]*$/, "", current_sec) in_target = (sec_filter == "" || current_sec == sec_filter) hdr_printed = 0 @@ -285,7 +374,8 @@ conf_dump() next } - # Commented-out key=value — shown only with show_all. + # Commented-out key=value — shown only with show_all, and only when + # the key is not set for real anywhere in the same section. # The value displayed is the live environment value (what load_conf # actually exported); when the key is not set in the environment we # fall back to the value written in the comment, i.e. the documented @@ -293,7 +383,7 @@ conf_dump() if (show_all && $0 ~ /^[[:space:]]*#[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) { line = $0; sub(/^[[:space:]]*#[[:space:]]*/, "", line) key = line; sub(/[[:space:]]*=.*$/, "", key); sub(/^[[:space:]]*/, "", key) - if (seen[current_sec ":" key]) next + if (active[current_sec ":" key] || seen[current_sec ":" key]) next seen[current_sec ":" key] = 1 if (key_filter != "" && index(key, key_filter) == 0) next val = ENVIRON[key] @@ -306,7 +396,7 @@ conf_dump() print " " c_default key c_rst "=" val } } - ' "$conf_file" + ' "$conf_file" "$conf_file" } export -f conf_dump # ------------------------------------------------------------------------------ @@ -318,9 +408,11 @@ export -f conf_dump # # If TERM is already set to a specific value (not empty, not the sentinel # "smart"), it is honoured as-is — standard Unix behaviour. -# If TERM is empty or set to "smart", terminal emulator hints are checked first -# (COLORTERM, TERM_PROGRAM, VTE_VERSION, WT_SESSION, TMUX, STY), then terminfo -# is probed in preference order: xterm-256color → xterm-color → xterm → vt100. +# If TERM is empty or set to "smart", multiplexer hints are checked first +# (TMUX, STY), then terminal emulator hints (TERM_PROGRAM, VTE_VERSION, +# WT_SESSION), then terminfo is probed in preference order: +# xterm-256color → xterm-color → xterm → vt100. COLORTERM=truecolor|24bit +# enables the *-direct (24-bit) entries in those probes. # # Usage: term_set term_set() @@ -334,27 +426,46 @@ term_set() fi # Return true when terminfo has an entry for the given terminal type. + # (Nested functions are global in bash; it is removed with unset -f below.) local _term_has _term_has() { tput -T "$1" longname >/dev/null 2>&1 } - # True-color hint: COLORTERM=truecolor|24bit is set by the emulator. + # True-color flag: COLORTERM=truecolor|24bit is set by the emulator. It does + # not select an entry by itself; it only enables the *-direct entries in the + # probes below, each of which falls back to its *-256color counterpart. local _truecolor=0 [[ "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && _truecolor=1 local _candidate="" - # 1. Explicit truecolor hint set by modern terminal emulators. - if (( _truecolor )); then - local _t - for _t in xterm-direct xterm-256color; do - _term_has "$_t" && { _candidate="$_t"; break; } - done + # Probes run in priority order and stop at the first terminfo entry found + # (_candidate non-empty). Multiplexers come first: they provide their own + # terminal and inherit the emulator's variables (TERM_PROGRAM, VTE_VERSION, + # ...), which would otherwise wrongly select the outer emulator's entry. + + # 1. tmux — prefer *-direct when truecolor, then *-256color, then screen-256color. + if [[ -n "${TMUX:-}" ]]; then + if (( _truecolor )); then + _term_has "tmux-direct" && _candidate="tmux-direct" + [[ -z "$_candidate" ]] && _term_has "screen-direct" && _candidate="screen-direct" + fi + [[ -z "$_candidate" ]] && _term_has "tmux-256color" && _candidate="tmux-256color" + [[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color" fi - # 2. Terminal programme name hints. + # 2. GNU screen — prefer screen-direct when truecolor, then screen-256color. + if [[ -z "$_candidate" && -n "${STY:-}" ]]; then + if (( _truecolor )); then + _term_has "screen-direct" && _candidate="screen-direct" + fi + [[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color" + [[ -z "$_candidate" ]] && _term_has "screen" && _candidate="screen" + fi + + # 3. Terminal programme name hints. if [[ -z "$_candidate" ]]; then case "${TERM_PROGRAM:-}" in iTerm.app) @@ -381,7 +492,7 @@ term_set() esac fi - # 3. VTE-based terminals (GNOME Terminal, Tilix, Xfce Terminal, …). + # 4. VTE-based terminals (GNOME Terminal, Tilix, Xfce Terminal, …). if [[ -z "$_candidate" && -n "${VTE_VERSION:-}" ]]; then if (( _truecolor )); then _term_has "vte-direct" && _candidate="vte-direct" @@ -390,7 +501,7 @@ term_set() [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" fi - # 4. Windows Terminal. + # 5. Windows Terminal. if [[ -z "$_candidate" && -n "${WT_SESSION:-}" ]]; then if (( _truecolor )); then _term_has "xterm-direct" && _candidate="xterm-direct" @@ -398,26 +509,7 @@ term_set() [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" fi - # 5. tmux — prefer *-direct when truecolor, then *-256color, then screen-256color. - if [[ -z "$_candidate" && -n "${TMUX:-}" ]]; then - if (( _truecolor )); then - _term_has "tmux-direct" && _candidate="tmux-direct" - [[ -z "$_candidate" ]] && _term_has "screen-direct" && _candidate="screen-direct" - fi - [[ -z "$_candidate" ]] && _term_has "tmux-256color" && _candidate="tmux-256color" - [[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color" - fi - - # 6. GNU screen — prefer screen-direct when truecolor, then screen-256color. - if [[ -z "$_candidate" && -n "${STY:-}" ]]; then - if (( _truecolor )); then - _term_has "screen-direct" && _candidate="screen-direct" - fi - [[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color" - [[ -z "$_candidate" ]] && _term_has "screen" && _candidate="screen" - fi - - # 7. Generic terminfo probe in preference order. + # 6. Generic terminfo probe in preference order. if [[ -z "$_candidate" ]]; then local _t for _t in xterm-256color xterm-color xterm vt100; do