fix conf management

This commit is contained in:
2026-10-13 11:49:40 +02:00
parent 6bfc2804ab
commit daa2ee755c
4 changed files with 192 additions and 86 deletions
+15 -13
View File
@@ -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 <section> <key> <value>`; 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 <section> <key> <value>`; 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 <seconds>` / `--timeout <seconds>` 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`).
+11
View File
@@ -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.
+3 -2
View File
@@ -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.
---
Executable → Regular
+163 -71
View File
@@ -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> <key> <value>
# 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