Files
lpic-sim/bin/_functions.sh
T
Björn RundenandClaude Opus 5.5 7f6b8ebe07 Basis aus 2nd-brain für LPIC-Prüfungsvorbereitung übernommen
Regeln, Standards, Kontextstruktur und Werkzeuge übernommen; 2nd-brain-
Inhalte (Stacks, Ansible-Inventar, Architektur, Knowledge) entfernt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:56:53 +01:00

416 lines
15 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#-------------------------------------------------------------------------------
## @file _functions.sh
## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap
## @author <AUTOR – siehe ai/context/responsible.md>
## @owner <VERANTWORTLICH – siehe ai/context/responsible.md>
## @version 1.0.0 (07.10.2026)
##
## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}"
## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4.
## Kopien NICHT lokal ändern – Projektspezifisches gehört in
## _functions.local.sh (wird automatisch geladen, falls vorhanden).
## Standard: ai/common/bash.md
#-------------------------------------------------------------------------------
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
echo "_functions.sh wird gesourct, nicht ausgeführt." >&2
exit 2
fi
[[ -n "${_B_LOADED:-}" ]] && return 0
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2
return 1
fi
_B_LOADED=1
B_FUNCTIONS_VERSION="1.0.0"
# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ----------------------
# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback.
: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}"
: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}"
: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}"
B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")"
B_LIB_DIR="${B_LIB_DIR%/*}"
B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde
B_TMP_DIR="" # wird von b_tmpdir gesetzt
# --- Standard-Optionen (gelten für jedes Script) ------------------------------
# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
# Variable im Script: OPTION_<LANG> (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN
B_OPTS_STD=(
"h|help|flag|0|diese Hilfe anzeigen"
"d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)"
"q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)"
"n|dry-run|flag|0|nur anzeigen, nichts ändern"
"l|logfile|value:DATEI||zusätzlich in DATEI loggen"
)
declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an
declare -ga ARGS=() # Positionsargumente nach b_init
declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40)
declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=()
declare -ga _B_OPT_ORDER=()
#===============================================================================
# Ausgabe / Logging
#===============================================================================
## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level
## @param $1 Level: debug | info | warn | error
## @param $@ Text
## @return 0 ok, 2 unbekanntes Level
blog() {
local level="${1:-}"; shift || true
local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r=""
[[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; }
if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then
if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then
r=$'\e[0m'
case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac
fi
case "$level" in
info) printf '%s\n' "$msg" ;;
debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;;
warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;;
error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;;
esac
fi
if [[ -n "${B_LOG_FILE:-}" ]] \
&& (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then
printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \
>> "$B_LOG_FILE" 2>/dev/null || true
fi
return 0
}
## @brief Ersatz für echo: Info-Meldung über blog
## @param $@ Text
## @return 0
becho() { blog info "$@"; }
## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug)
## @param $@ Text
## @return 0
bdebug() { blog debug "$@"; }
## @brief Warnung (stderr)
## @param $@ Text
## @return 0
bwarn() { blog warn "$@"; }
## @brief Fehlermeldung (stderr)
## @param $@ Text
## @return 0
berror() { blog error "$@"; }
## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf)
## @param $1 Text
## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis)
## @return – (beendet das Script)
bdie() {
local rc="${2:-1}"
berror "${1:-Abbruch}"
(( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help"
exit "$rc"
}
#===============================================================================
# Hilfen für den Script-Alltag
#===============================================================================
## @brief Befehl ausführen – oder bei --dry-run nur anzeigen
## @param $@ Befehl mit Argumenten (kein String, kein eval)
## @return Exit-Code des Befehls, 0 im Dry-Run
brun() {
if (( ${OPTION_DRY_RUN:-0} )); then
becho "[dry-run] $(printf '%q ' "$@")"
return 0
fi
bdebug "run: $(printf '%q ' "$@")"
"$@"
}
## @brief Prüft, ob Programme vorhanden sind
## @param $@ Programmnamen
## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet)
b_require() {
local cmd rc=0
for cmd in "$@"; do
command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; }
done
return "$rc"
}
## @brief Prüft, ob Variablen gesetzt und nicht leer sind
## @param $@ Variablennamen
## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet)
b_require_vars() {
local v rc=0
for v in "$@"; do
[[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; }
done
return "$rc"
}
## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht
## @return 0 ok, 1 mktemp fehlgeschlagen
b_tmpdir() {
[[ -n "$B_TMP_DIR" ]] && return 0
B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1
bdebug "Temp-Verzeichnis: $B_TMP_DIR"
}
## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2
## @param $1 Pfad
## @param $2 Basisverzeichnis (Default: SCRIPT_DIR)
## @return 0; Ergebnis auf stdout
b_abspath() {
local p="$1" base="${2:-$SCRIPT_DIR}"
[[ "$p" == /* ]] || p="${base%/}/${p}"
printf '%s\n' "$p"
}
#===============================================================================
# Usage aus der Präambel
#===============================================================================
## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt,
## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt)
## @return 0
b_usage() {
local line part=0
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^#-{10,} ]]; then
(( ++part >= 2 )) && break
continue
fi
(( part == 1 )) && [[ "$line" == '##'* ]] || continue
line="${line#'##'}"; line="${line# }"
if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi
done < "$SCRIPT_PATH"
return 0
}
## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS)
## @return 0
_b_opts_help() {
local long s left d
for long in "${_B_OPT_ORDER[@]}"; do
s="${_B_OPT_SHORT[$long]}"
left="${s:+-$s | }--${long}"
[[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}"
d="${_B_OPT_DEFAULT[$long]}"
if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then
printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d"
else
printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}"
fi
done
return 0
}
#===============================================================================
# Optionen
#===============================================================================
## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN
## @param $1 Langname
## @return 0; Name auf stdout
_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; }
## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie
## @return 0 ok, 2 Definitionsfehler
_b_opts_build() {
local def s long type meta deflt desc rest
local -A seen_short=()
_B_OPT_ORDER=()
for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do
IFS='|' read -r s long type deflt desc rest <<< "$def"
meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT"
if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then
berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)"
return 2
fi
if [[ -n "$s" ]]; then
[[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && {
berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; }
seen_short[$s]="$long"
fi
[[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long")
_B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta"
_B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc"
done
return 0
}
## @brief Setzt OPTION_<LANG> für eine erkannte Option
## @param $1 Langname
## @param $2 Wert
## @return 0 ok, 2 ungültiger Wert
_b_opt_set() {
local long="$1" val="$2" var
var="$(_b_optvar "$long")"
if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then
berror "Option --$long erwartet 0 oder 1, nicht \"$val\""
return 2
fi
printf -v "$var" '%s' "$val"
_B_CLI_SET[$long]=1
return 0
}
## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente)
## @param $@ Argumente des Scripts
## @return 0 ok, 2 Aufruffehler
_b_opts_parse() {
local arg name val s i long
ARGS=()
while (( $# )); do
arg="$1"; shift
case "$arg" in
--) ARGS+=("$@"); break ;;
--?*)
name="${arg#--}"; val=""
if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi
[[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; }
if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then
(( $# )) || { berror "Option --$name braucht einen Wert"; return 2; }
val="$1"; shift
elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then
val=1
fi
_b_opt_set "$name" "$val" || return 2
;;
-?*)
for (( i = 1; i < ${#arg}; i++ )); do
s="${arg:i:1}"; long=""
for name in "${_B_OPT_ORDER[@]}"; do
[[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; }
done
[[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; }
if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then
val="${arg:i+1}" # -lDATEI oder -l DATEI
if [[ -z "$val" ]]; then
(( $# )) || { berror "Option -$s braucht einen Wert"; return 2; }
val="$1"; shift
fi
_b_opt_set "$long" "$val" || return 2
break
fi
_b_opt_set "$long" 1 || return 2
done
;;
*) ARGS+=("$arg") ;;
esac
done
return 0
}
#===============================================================================
# Trap / Aufräumen
#===============================================================================
## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR
## @return – (beendet mit dem ursprünglichen Exit-Code)
_b_on_exit() {
local rc=$?
trap - EXIT
set +e
if declare -F cleanup &>/dev/null; then
cleanup "$rc" || bwarn "cleanup() meldete Fehler"
fi
[[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR"
bdebug "Ende ${SCRIPT_NAME} (rc=${rc})"
exit "$rc"
}
## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile
## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug)
## @param $1 Zeilennummer
## @param $2 Befehl
## @return 0 (Abbruch übernimmt set -e)
_b_on_err() {
local rc=$? lvl=error
[[ "$2" == return* ]] && lvl=debug
blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}"
return 0
}
#===============================================================================
# Einstieg
#===============================================================================
## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe
## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile
## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}")
## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2)
b_init() {
local v long var env_file
local -A env_keep=()
declare -gA _B_CLI_SET=()
trap _b_on_exit EXIT
trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR
trap 'exit 130' INT
trap 'exit 143' TERM
# 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env)
for v in $(compgen -e); do
[[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}"
done
# 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD)
env_file="$(b_abspath "${B_ENV_FILE:-.env}")"
if [[ -f "$env_file" ]]; then
# shellcheck source=/dev/null
source "$env_file" || bdie "Fehler beim Laden von $env_file"
fi
for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done
# 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat
: "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \
"${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}"
[[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; }
[[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")"
_b_opts_build || exit 2
for long in "${_B_OPT_ORDER[@]}"; do
var="$(_b_optvar "$long")"
[[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}"
done
# 4. Kommandozeile
_b_opts_parse "$@" || bdie "Aufruffehler" 2
# 5. Standard-Optionen anwenden
(( OPTION_DEBUG )) && B_LOG_LEVEL=debug
(( OPTION_QUIET )) && B_LOG_CONSOLE=0
if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then
B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD
elif [[ -n "$OPTION_LOGFILE" ]]; then
B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")"
fi
if [[ -n "$B_LOG_FILE" ]]; then
mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \
|| { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; }
fi
(( OPTION_HELP )) && { b_usage; exit 0; }
bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}"
bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}"
for long in "${_B_OPT_ORDER[@]}"; do
var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}"
done
return 0
}
# --- projektspezifische Erweiterungen -----------------------------------------
if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then
# shellcheck source=/dev/null
source "${B_LIB_DIR}/_functions.local.sh"
fi