Script De Base Pour Bash

Je présente ici le point de départ pour un script Bash à même de gérer un fichier de configuration, des paramètres par défaut ainsi que des paramètres fourni au lancement

#!/usr/bin/env bash
# ==============================================================================
# mon_script.sh — Template de script Bash défensif
# ==============================================================================
# Usage : mon_script.sh [OPTIONS]
#
# Options :
#   -a, --append  <valeur>   Paramètre append (obligatoire)
#   -e, --exec               Active le mode exécution
#   -h, --help               Affiche cette aide
#   --                       Fin des options
#
# Fichier de configuration : ~/mon_scriptrc.cfg
#   Format : parm=valeur (une par ligne, # pour commenter)
#
# Codes de retour :
#   0   Succès
#   1   Erreur de paramètre / usage
#   2   Erreur d'environnement (dépendance manquante, droits, etc.)
#   3   Erreur d'exécution métier
# ==============================================================================

# ==============================================================================
# Sécurisation de l'exécution
# ==============================================================================
set -e          # Arrêt immédiat sur erreur non gérée
set -u          # Arrêt sur variable non définie
set -o pipefail # Arrêt si une commande dans un pipe échoue
# set -x        # Décommenter pour le debug (trace chaque commande)

# Garantit que le script ne tourne pas en root sauf besoin explicite
# if [[ $EUID -eq 0 ]]; then
#     echo "Erreur : ce script ne doit pas être exécuté en root." >&2
#     exit 2
# fi

# ==============================================================================
# Constantes globales (en MAJUSCULES, readonly)
# ==============================================================================
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly SCRIPT_PID=$$
readonly SCRIPT_VERSION="1.0.0"
readonly TIMESTAMP=$(date +"%Y%m%d_%H%M%S")

readonly CONFIG_FIC="${HOME}/.mon_scriptrc.cfg"
readonly TMP_DIR=$(mktemp -d /tmp/"${SCRIPT_NAME}_${SCRIPT_PID}")
readonly LOG_FILE="${TMP_DIR}/${SCRIPT_NAME}_${TIMESTAMP}.log"

# Codes de retour nommés
readonly RC_OK=0
readonly RC_USAGE=1
readonly RC_ENV=2
readonly RC_EXEC=3

# ==============================================================================
# Valeurs par défaut des paramètres (toutes initialisées — protection set -u)
# ==============================================================================
a=""
b="blabla"
c="machine"
e=false

# ==============================================================================
# Gestion du nettoyage à la sortie (trap)
# ==============================================================================

function nettoyage {
    local rc=$?
    # Supression systématique du répertoire temporaire, même en cas d'erreur
    if [[ -d "${TMP_DIR}" ]]; then
        rm -rf "${TMP_DIR}"
    fi
    if [[ $rc -ne 0 ]]; then
        log "ERROR" "Script terminé avec le code $rc"
    fi
    exit $rc
}

# Intercepte : sortie normale, erreur, CTRL+C, kill, erreur de syntaxe
trap nettoyage EXIT
trap 'log "ERROR" "Signal SIGINT reçu (CTRL+C)" ; exit $RC_EXEC' INT
trap 'log "ERROR" "Signal SIGTERM reçu"          ; exit $RC_EXEC' TERM
trap 'log "ERROR" "Erreur ligne $LINENO — commande : $BASH_COMMAND" ; exit $RC_EXEC' ERR

# ==============================================================================
# Fonctions utilitaires
# ==============================================================================

# Journalisation horodatée sur stderr + fichier log
function log {
    local niveau="${1:-INFO}"
    local message="${2:-}"
    local ts
    ts=$(date +"%Y-%m-%d %H:%M:%S")
    printf "[%s] [%s] [PID:%s] %s\n" "$ts" "$niveau" "$SCRIPT_PID" "$message" \
        | tee -a "${LOG_FILE}" >&2
}

# Affichage de l'aide (reprend l'en-tête du script)
function utilisation {
    grep "^#" "$0" | grep -v "^#!/" | sed 's/^# \{0,1\}//' | \
        sed -n '/^Usage/,/^Codes de retour/{ /^Codes de retour/q; p }'
    echo ""
    echo "Usage : ${SCRIPT_NAME} -a <valeur> [-e] [-h]"
    echo ""
}

# Vérification des dépendances externes requises par le script
function verifier_dependances {
    local deps=("awk" "grep" "date" "mktemp")  # À compléter selon les besoins
    local manquantes=()

    for cmd in "${deps[@]}"; do
        if ! command -v "$cmd" &>/dev/null; then
            manquantes+=("$cmd")
        fi
    done

    if [[ ${#manquantes[@]} -gt 0 ]]; then
        log "ERROR" "Dépendances manquantes : ${manquantes[*]}"
        return $RC_ENV
    fi
}

# ==============================================================================
# Contrôle des paramètres obligatoires
# ==============================================================================

function controle_parm {
    local rc=$RC_OK
    local erreurs=()

    # Paramètre obligatoire
    if [[ -z "${a}" ]]; then
        erreurs+=("  --append (-a) est obligatoire")
        rc=$RC_USAGE
    fi

    # Contrôles métier additionnels (exemples)
    # if [[ "${a}" =~ [^a-zA-Z0-9_-] ]]; then
    #     erreurs+=("  --append : caractères non autorisés dans '${a}'")
    #     rc=$RC_USAGE
    # fi
    # if [[ ${#a} -gt 64 ]]; then
    #     erreurs+=("  --append : valeur trop longue (max 64 caractères)")
    #     rc=$RC_USAGE
    # fi

    if [[ $rc -ne $RC_OK ]]; then
        log "ERROR" "Paramètres invalides :"
        for err in "${erreurs[@]}"; do
            log "ERROR" "$err"
        done
        utilisation
    fi

    return $rc
}

# ==============================================================================
# Chargement du fichier de configuration
# ==============================================================================

function charger_config {
    if [[ ! -f "${CONFIG_FIC}" ]]; then
        log "INFO" "Pas de fichier de configuration trouvé (${CONFIG_FIC}), valeurs par défaut utilisées"
        return $RC_OK
    fi

    # Vérification des droits : le fichier ne doit pas être lisible par tous
    if [[ "$(stat -c '%a' "${CONFIG_FIC}" 2>/dev/null)" =~ ^.[0-9][0-9]$ ]]; then
        log "WARN" "Le fichier de configuration est lisible par d'autres utilisateurs ($(stat -c '%a' "${CONFIG_FIC}"))"
    fi

    local tmp_cfg="${TMP_DIR}/config.tmp"

    # Filtrage : suppression commentaires (#) et lignes vides
    grep -v '^\s*#' "${CONFIG_FIC}" | grep -v '^\s*$' > "${tmp_cfg}" || true

    local ligne parm val num_ligne=0
    while IFS= read -r ligne; do
        num_ligne=$((num_ligne + 1))

        # Vérification du format parm=val
        if [[ ! "${ligne}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*=.*$ ]]; then
            log "WARN" "Ligne ${num_ligne} ignorée (format invalide) : '${ligne}'"
            continue
        fi

        parm="${ligne%%=*}"          # Tout avant le premier '='
        val="${ligne#*=}"            # Tout après le premier '='
        val="${val%\"}"              # Supprime guillemet final éventuel
        val="${val#\"}"              # Supprime guillemet initial éventuel

        case "${parm}" in
            append)
                a="${val}"
                log "INFO" "Config : append chargé"
                ;;
            *)
                log "WARN" "Paramètre inconnu dans la config : '${parm}' (ligne ${num_ligne})"
                ;;
        esac
    done < "${tmp_cfg}"
}

# ==============================================================================
# Parsing des arguments en ligne de commande
# ==============================================================================

function parser_arguments {
    while [[ $# -gt 0 ]]; do
        case "$1" in

            -a|--append)
                if [[ $# -lt 2 ]]; then
                    log "ERROR" "L'option $1 attend une valeur"
                    utilisation
                    return $RC_USAGE
                fi
                if [[ "$2" == -* ]]; then
                    log "ERROR" "Valeur invalide pour $1 : '$2' ressemble à une option"
                    utilisation
                    return $RC_USAGE
                fi
                if [[ -z "$2" ]]; then
                    log "ERROR" "La valeur de $1 ne peut pas être vide"
                    utilisation
                    return $RC_USAGE
                fi
                shift
                a="$1"
                shift
                ;;

            -e|--exec)
                e=true
                shift
                ;;

            -h|--help)
                utilisation
                exit $RC_OK
                ;;

            --)
                shift
                break   # Tout ce qui suit '--' est argument positionnel
                ;;

            -*)
                log "ERROR" "Option inconnue : '$1'"
                utilisation
                return $RC_USAGE
                ;;

            *)
                log "ERROR" "Argument inattendu : '$1'"
                utilisation
                return $RC_USAGE
                ;;

        esac
    done

    # Traitement des arguments positionnels résiduels (après --)
    # if [[ $# -gt 0 ]]; then
    #     log "ERROR" "Arguments positionnels non attendus : $*"
    #     return $RC_USAGE
    # fi
}

# ==============================================================================
# Corps principal
# ==============================================================================

function main {
    log "INFO" "Démarrage de ${SCRIPT_NAME} v${SCRIPT_VERSION}"

    # 1. Vérification de l'environnement
    verifier_dependances || exit $RC_ENV

    # 2. Chargement de la configuration (les options CLI écrasent la config)
    charger_config

    # 3. Parsing des arguments CLI
    parser_arguments "$@" || exit $RC_USAGE

    # 4. Contrôle des paramètres obligatoires
    controle_parm || exit $RC_USAGE

    log "INFO" "Paramètres validés — append='${a}' exec=${e}"

    # ==========================================================================
    # Début de la logique métier
    # ==========================================================================



    # ==========================================================================
    # Fin de la logique métier
    # ==========================================================================

    log "INFO" "${SCRIPT_NAME} terminé avec succès"
    return $RC_OK
}

# Point d'entrée : on passe tous les arguments à main
main "$@"