#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # Protected by the BSD3 license. Please read bellow for details. # # * Redistribution and use in source and binary forms, # * with or without modification, are permitted provided # * that the following conditions are met: # * # * Redistributions of source code must retain the above # * copyright notice, this list of conditions and the # * following disclaimer. # * # * Redistributions in binary form must reproduce the above # * copyright notice, this list of conditions and the following # * disclaimer in the documentation and/or other materials # * provided with the distribution. # * # * Neither the name of the copyright holder nor the names # * of any other contributors may be used to endorse or # * promote products derived from this software without # * specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR # * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ # Common helpers, then the ones private to this module (helpers/conf.sh) declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh" _profile_load_helpers "${BASH_SOURCE[0]}" # ------------------------------------------------------------------------------ # 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, must be a single line) conf_save() { if [[ $# -ne 3 ]]; then disp E "Usage: conf_save
" return 1 fi local section="$1" key="$2" value="$3" if ! [[ "$section" =~ ^[a-zA-Z0-9_]+$ ]]; then disp E "conf_save: invalid section name '${section}' (alphanumeric and underscore only)." return 1 fi if ! [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then disp E "conf_save: invalid key name '${key}' (alphanumeric and underscore only)." return 1 fi # 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_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 } if [[ ! -e "$conf_file" ]]; then { printf "[%s]\n" "$section" printf "%s=%s\n" "$key" "$value" } > "$conf_file" || { disp E "conf_save: unable to write configuration file: ${conf_file}" return 1 } 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 } { # 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 ~ ("^[[:space:]]*\\[" sec "\\][[:space:]]*$")) { in_sec = 1 saw_sec = 1 } else { in_sec = 0 } print 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 wrote = 1 } next } print } END { if (in_sec && !wrote) { print key "=" val wrote = 1 } flush_pending() if (!saw_sec) { if (NR > 0) print "" print "[" sec "]" print key "=" val } } ' "$conf_file" > "$tmp_file" || { rm -f "$tmp_file" disp E "conf_save: unable to update configuration file: ${conf_file}" return 1 } # 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 } } export -f conf_save # ------------------------------------------------------------------------------ # Display the profile configuration file, with optional section and key filters. # The same file resolution as conf_save is used: $HOME/.profile.conf when # present, otherwise $PROFILE_CONF or $MYPATH/profile.conf. # # Usage: conf_dump [options] [pattern] # -s, --section NAME : Only display the given section # -a, --all : Also show commented-out keys (default values, in white) # pattern : Only display keys whose name contains this substring # # Output colours: # green — key is explicitly set (uncommented) in the config file # white — key is present but commented out (shows the default value) conf_dump() { local section="" key_pattern="" show_all=0 local PARSED PARSED=$(getopt -o hs:a --long help,section:,all -n 'conf_dump' -- "$@") # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"conf_dump --help\" to display usage." return 1 fi # Restore the getopt-normalised (shell-quoted) argument list, "--" ends options local -a _pargs=() _getopt_split _pargs "$PARSED" set -- "${_pargs[@]}" while true; do case "$1" in -h|--help) printf "conf_dump: Display the profile configuration file, with optional filters.\n\n" printf "Usage: conf_dump [options] [pattern]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-s, --section NAME\tOnly display the given section\n" printf "\t-a, --all\t\tAlso show commented-out keys (default values)\n\n" printf "Arguments:\n" printf "\tpattern\tOnly display keys whose name contains this substring\n\n" printf "Output colours:\n" printf "\t\033[1;32mgreen\033[0m — key is explicitly set (active)\n" printf "\t\033[1;97mwhite\033[0m — key is commented out (default value)\n" return 0 ;; -s|--section) section="$2" shift 2 ;; -a|--all) show_all=1 shift ;; --) shift break ;; *) disp E "Invalid option, use \"conf_dump --help\" to display options list" return 1 ;; esac done [[ $# -gt 0 ]] && key_pattern="$1" local conf_file conf_file=$(_conf_path) if [[ ! -f "$conf_file" ]]; then disp E "conf_dump: configuration file not found: ${conf_file}" return 1 fi # 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"] 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) gsub(/\\e/, "\033", c_active) gsub(/\\e/, "\033", c_default) gsub(/\\e/, "\033", c_rst) in_target = 0 current_sec = "" hdr_printed = 0 found = 0 # 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 (leading blanks allowed, as parse_conf does) if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) { current_sec = $0 sub(/^[[:space:]]*\[/, "", current_sec) sub(/\][[:space:]]*$/, "", current_sec) in_target = (sec_filter == "" || current_sec == sec_filter) hdr_printed = 0 next } if (!in_target) next # Active (uncommented) key=value — always shown if ($0 ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) { key = $0; sub(/[[:space:]]*=.*$/, "", key); sub(/^[[:space:]]*/, "", key) val = $0; sub(/^[^=]*=/, "", val) if (key_filter != "" && index(key, key_filter) == 0) next if (!hdr_printed) { if (found) print "" print c_sec "[" current_sec "]" c_rst hdr_printed = 1; found = 1 } seen[current_sec ":" key] = 1 print " " c_active key c_rst "=" val next } # 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 # default. Each key is shown at most once. 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 (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] if (val == "") { val = line; sub(/^[^=]*=[[:space:]]*/, "", val) } if (!hdr_printed) { if (found) print "" print c_sec "[" current_sec "]" c_rst hdr_printed = 1; found = 1 } print " " c_default key c_rst "=" val } } ' "$conf_file" "$conf_file" } export -f conf_dump # ------------------------------------------------------------------------------ # Set TERM to the best terminal capability available on this system. # Called automatically at sourcing time. # # 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", 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() { local _current="${TERM:-}" # Specific value already set — nothing to do. if [[ -n "$_current" && "$_current" != "smart" ]]; then export TERM="$_current" return 0 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 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="" # 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. 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) if (( _truecolor )); then _term_has "xterm-direct" && _candidate="xterm-direct" fi [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" ;; WezTerm) if (( _truecolor )); then _term_has "xterm-direct" && _candidate="xterm-direct" fi if [[ -z "$_candidate" ]]; then _term_has "wezterm" && _candidate="wezterm" fi [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" ;; Hyper|vscode) if (( _truecolor )); then _term_has "xterm-direct" && _candidate="xterm-direct" fi [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" ;; esac fi # 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" fi [[ -z "$_candidate" ]] && _term_has "vte-256color" && _candidate="vte-256color" [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" fi # 5. Windows Terminal. if [[ -z "$_candidate" && -n "${WT_SESSION:-}" ]]; then if (( _truecolor )); then _term_has "xterm-direct" && _candidate="xterm-direct" fi [[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color" fi # 6. Generic terminfo probe in preference order. if [[ -z "$_candidate" ]]; then local _t for _t in xterm-256color xterm-color xterm vt100; do _term_has "$_t" && { _candidate="$_t"; break; } done fi unset -f _term_has export TERM="${_candidate:-vt100}" } export -f term_set # ------------------------------------------------------------------------------ term_set # EOF