##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 ## Gestion de fichiers de configuration et de répertoires de configuration ##@cooked nocomments uprovide multiconf urequire install function conf_local() { # afficher les commandes pour définir comme locales les variables utilisées # par les fonctions conf_* # cela permet d'utiliser ces fonctions à l'intérieur d'autres fonctions sans # polluer l'espace de nom echo "local -a __CONF_DESCS __CONF_ARRAY_VARS __CONF_PATH_VARS" } function conf_auto() { # charger la configuration pour l'outil $1 avec les variables $2..@ # conf_init n'est appelé que si des variables sont spécifiées, ce qui permet # d'appeler conf_init au préalable si une configuration spécifique doit être # faite. # Ainsi: # conf_auto NAME VARS... # est équivalent à: # conf_init VARS... # conf_auto NAME # est équivalent à: # conf_init VARS... # conf_find_files __CONF_FILES ~/etc/default/NAME ~/etc/NAME.d/*.conf # conf_upgrade ~/etc/default/NAME # conf_load_files "${__CONF_FILES[@]}" # Pour supporter les scénarii où les fichiers de configuration sont ailleurs # que dans ~/etc/default, l'argument NAME peut être un chemin: # conf_auto PATH/TO/NAME VARS... # est équivalent à: # conf_init VARS... # conf_find_files __CONF_FILES PATH/TO/NAME.conf PATH/TO/NAME.d/*.conf # conf_upgrade PATH/TO/NAME.conf # conf_load_files "${__CONF_FILES[@]}" local __name="$1"; shift [ -n "$__name" ] || return 1 [ $# -gt 0 ] && conf_init "$@" local -a __CONF_FILES if [[ "$__name" == */* ]]; then conf_find_files __CONFS_FILES "$__name.conf" "$__name.d/*.conf" [ $# -gt 0 ] && conf_upgrade "$__name.conf" else conf_find_files __CONFS_FILES "$HOME/etc/default/$__name" "$HOME/etc/$__name.d/*.conf" [ $# -gt 0 ] && conf_upgrade "$HOME/etc/default/$__name" fi conf_load_files "${__CONFS_FILES[@]}" } function conf_init() { # définir les variables attendues lors du chargement des fichiers de # configuration par conf_load_files() # Si cette fonction n'a pas d'argument, le contenu du tableau CONFIG s'il # est existe est utilisé # par défaut, les variables sont en mode scalaire: la définition d'une # variable écrase la valeur précédente. Avec l'option -a les variables sont # en mode tableau: les nouvelles valeurs sont rajoutées à la fin du tableau. # Avec l'option -p les variables sont en mode chemin: les nouvelles valeurs # sont ajoutées si elles n'existe pas déjà avec le séparateur ':' # dans l'exemple suivant: # conf_init NAME VALUE -a SRCDIRS DESTDIRS -p LIBPATH # NAME et VALUE sont scalaires, SRCDIRS et DESTDIRS sont des tableaux et # LIBPATH est de type chemin # Les variables scalaires et chemin sont initialisées à la valeur vide ou à # la valeur spécifiée e.g.: # conf_init VAR=value MYPATH=a:b:c # Les variables tableaux sont initialisées à la valeur vide sauf si le nom # est suivi de '=' auquel cas la valeur actuelle est gardée, e.g. # VS=(a b c); WS=(x y z) # conf_init -a VS WS= # echo_seta2 VS # VS=() # echo_seta2 WS # WS=(x y z) # Dans le cas des variables tableaux, la valeur après '=' est toujours # ignorée. Pour simplifier la lecture, on peut rajouter une valeur marqueur # comme 'current' ou 'actual', e.g # conf_init -a VS=CurrentValues # L'option -s permet de revenir au mode scalaire # Note: il est possible d'associer une description à chaque variable ainsi # qu'un en-tête, ce qui permet de construire le fichier de configuration ou # de mettre à jour un fichier existant avec conf_upgrade(). Par exemple, les # commandes suivantes: # CONFIG=( # "# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" # "# configurer l'application" # -s # "NAME=payet//nom de l'administrateur" # "MAIL=admin@host.tld//mail de contact" # -a # "HOSTS//hôtes autorisés à se connecter" # ) # conf_init # permettent de générer automatiquement le fichier de configuration suivant: # # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 # # configurer l'application # # # nom de l'administrateur # #NAME=payet # # # mail de contact # #MAIL=admin@host.tld # # # hôtes autorisés à se connecter # #HOSTS=() __CONF_DESCS=() __CONF_ARRAY_VARS=() __CONF_PATH_VARS=() local __type=scalar __initial=1 __prefix __var __desc [ $# -eq 0 ] && is_array CONFIG && set -- "${CONFIG[@]}" while [ $# -gt 0 ]; do if [ -n "$__initial" ]; then if [ "${1:0:1}" == "#" ]; then [ ${#__prefix} -gt 0 ] && __prefix="$__prefix"$'\n' __prefix="$__prefix$1" shift continue else [ -n "$__prefix" ] && array_add __CONF_DESCS "$__prefix" __initial= fi fi case "$1" in -a|--array) __type=array;; -p|--path) __type=path;; -s|--scalar) __type=scalar;; *) array_add __CONF_DESCS "$1" splitfsep "$1" // __var __desc case "$__type" in array) [ "${__var%%=*}" == "$__var" ] && eval "${__var%%=*}=()" array_addu __CONF_ARRAY_VARS "${__var%%=*}" array_del __CONF_PATH_VARS "${__var%%=*}" ;; path) setv "$__var" array_addu __CONF_PATH_VARS "${__var%%=*}" array_del __CONF_ARRAY_VARS "${__var%%=*}" ;; scalar) setv "$__var" ;; esac ;; esac shift done } function conf_load() { # charger les fichiers de configuration spécifiés # conf_load SPECS... # est équivalent à: # conf_find_files __CONF_FILES SPECS... # conf_load_files "${__CONF_FILES[@]}" local -a __CONF_FILES conf_find_files __CONFS_FILES "$@" conf_load_files "${__CONFS_FILES[@]}" } function conf_find_files() { # initialiser le tableau $1 avec les fichiers de configuration correspondant # aux arguments $2..@ # - si on spécifie un fichier, il est pris tel quel s'il existe # - si on spécifie un répertoire, tous les fichiers *.conf de ce répertoire # sont pris # - si on spécifie un pattern e.g path/to/*.conf alors tous les fichiers # correspondant au pattern sont pris # - sinon l'argument est ignoré local __dest="$1"; shift local -a __files local __spec __dir __wc array_new "$__dest" for __spec in "$@"; do if [ -f "$__spec" ]; then array_add "$__dest" "$__spec" continue elif [ -d "$__spec" ]; then __spec="$__spec/*.conf" fi splitwcs "$__spec" __dir __wc array_lsfiles __files "${__dir:-.}" "$__wc" array_extend "$__dest" __files done } function conf_load_files() { # sourcer les fichiers spécifiés en faisant ce qui est nécessaire pour que # les variables de __CONF_ARRAY_VARS soient correctement traitées. local -a __backups __values local __file __name __i __backup __bn __bv for __file in "$@"; do # faire une copie de sauvegarde puis supprimer les variables tableaux __backups=() for __name in "${__CONF_ARRAY_VARS[@]}" "${__CONF_PATH_VARS[@]}"; do __backup="$(declare -p "$__name" 2>/dev/null)" if [ -z "$__backup" ]; then __backup="$__name=" else # faire une correction de l'expression parce que la commande # affichée par declare -p est différente entre bash 4.3 et bash # 4.4 pour les tableaux. soit le tableau array=(a b) # - bash 4.3 affiche declare -a array='([0]="a" [1]="b")' # - bash 4.4 affiche declare -a array=([0]="a" [1]="b") __backup="${__backup#declare }" __bn="${__backup%% *}" __bv="${__backup#* }" if [[ "$__bn" == -*a* ]]; then __bn="${__bv%%=*}" __bv="${__bv#*=}" if [ "${__bv:0:2}" == "'(" -a "${__bv: -2:2}" == ")'" ]; then __backup="$__bn=$(eval "echo $__bv")" else __backup="$__bn=$__bv" fi else __backup="$__bv" fi fi __backups=("${__backups[@]}" "$__backup") unset "$__name" done # charger le fichier source "$__file" # puis restaurer les variables ou les fusionner avec une éventuelle nouvelle valeur __i=0 for __name in "${__CONF_ARRAY_VARS[@]}" "${__CONF_PATH_VARS[@]}"; do __backup="${__backups[$__i]}" if [ -n "$(declare -p "$__name" 2>/dev/null)" ]; then # la variable a été redéfinie, la fusionner avec la précédente valeur if array_contains __CONF_ARRAY_VARS "$__name"; then array_copy __values "$__name" eval "$__backup" array_extend "$__name" __values elif array_contains __CONF_PATH_VARS "$__name"; then __values="${!__name}" eval "$__backup" uaddpath "$__values" "$__name" fi else # la variable n'a pas été redéfinie, restaurer la précédente valeur eval "$__backup" fi __i=$(($__i + 1)) done done } CONF_INSTALL_ASK_DEFAULT= function conf_install() { # USAGE: conf_install DEST PREFIX SRCS... # installer les fichiers de SRCS dans le répertoire standardisé DEST avec le # préfixe PREFIX # ## destination # - si DEST est un nom sans chemin, e.g NAME, alors la destination est # ~/etc/NAME.d # - si DEST est un nom avec chemin, alors la valeur est prise telle quelle # comme destination, et le répertoire est créé le cas échéant. # Si un fichier existe déjà dans la destination, afficher une demande de # confirmation avant de l'écraser # ## source # - si SRC est un fichier, le prendre tel quel # - si SRC est un répertoire, prendre tous les fichiers SRC/*.conf # - si SRC est un pattern, prendre tous les fichiers correspondant # s'il n'y a qu'une seule source, la destination sera DEST/PREFIX.conf # sinon, la destination sera DEST/PREFIX-SRCNAME où SRCNAME est le nom du # fichier source # si PREFIX est vide, alors les fichiers sont copiés avec leur nom sans # modification. local -a tmpsrcs srcs local src dir wc local dest="$1"; shift local prefix="$1"; shift [[ "$dest" == */* ]] || dest="$HOME/etc/$dest.d" mkdir -p "$dest" || return 1 for src in "$@"; do if [ -f "$src" ]; then array_add srcs "$src" elif [ -d "$src" ]; then array_lsfiles tmpsrcs "$src" "*.conf" array_extend srcs tmpsrcs else splitwcs "$src" dir wc array_lsfiles tmpsrcs "$dir" "$wc" array_extend srcs tmpsrcs fi done [ ${#srcs[*]} -gt 0 ] || return 0 local COPY_UPDATE_ASK_DEFAULT="${CONF_INSTALL_ASK_DEFAULT:-$COPY_UPDATE_ASK_DEFAULT}" if [ -n "$prefix" ]; then if [ ${#srcs[*]} -eq 1 ]; then copy_update_ask -y "$src" "$dest/$prefix.conf" else for src in "${srcs[@]}"; do copy_update_ask -y "$src" "$dest/$prefix-$(basename -- "$srcname")" done fi else for src in "${srcs[@]}"; do copy_update_ask -y "$src" "$dest" done fi } function conf_upgrade() { # USAGE: conf_upgrade DEST [VARS...] # Si les variables VARS... sont spécifiées, on appelle au préalable # conf_init() local __dest="$1"; shift local __desc __namevalue __name __value [ $# -gt 0 ] && conf_init "$@" # calculer le préfixe et initialiser le fichier le cas échéant if [ ! -f "$__dest" ]; then local __prefix for __desc in "${__CONF_DESCS[@]}"; do [ "${__desc:0:1}" == "#" ] && __prefix="$__desc" break done [ ${#__prefix} -gt 0 ] || __prefix="# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" echo "$__prefix" >"$__dest" fi # vérifier la présence de chaque variable for __desc in "${__CONF_DESCS[@]}"; do [ "${__desc:0:1}" == "#" ] && continue splitfsep "$__desc" // __namevalue __desc splitvar "$__namevalue" __name __value if ! grep -qE "^\s*#*(\s*export)?\s*$__name=" "$__dest"; then echo >>"$__dest" [ -n "$__desc" ] && echo "# $__desc" >>"$__dest" echo -n "#" >>"$__dest" if array_contains __CONF_ARRAY_VARS "$__name"; then echo_seta2 "$__name" >>"$__dest" else echo_setv "$__name" "$__value" >>"$__dest" fi fi done } function conf_update() { # USAGE: conf_update [-n] DEST VARS... # Pour chaque variable mentionnée, le fichier DEST est mis à jour avec sa # valeur actuelle. Le fichier doit exister, et conf_init() *doit* avoir été # appelé au préalable. # Avec l'option -n, ne pas modifier l'état activé/désactivé des variables # dans le fichier de configuration. En d'autres termes, si la variable était # commentée dans le fichier, la laisser commentée, mais mettre quand même à # jour la valeur local enable=1 if [ "$1" == -n ]; then enable= shift fi local dest="$1"; shift [ -f "$dest" ] || return 1 local from to ac_set_tmpfile destf ac_set_tmpfile destt cat "$dest" >"$destf" local name setvar for name in "$@"; do if array_contains __CONF_ARRAY_VARS "$name"; then setx setvar=echo_seta2 "$name" else setx setvar=echo_setv2 "$name" fi awkrun <"$destf" >"$destt" name="$name" setvar="$setvar" enable:int="$enable" ' $0 ~ "^\\s*#*(\\s*export)?\\s*" name "=" { match($0, "^(\\s*#*)((\\s*export)?\\s*)" name "=", vs) if (enable) print vs[2] setvar else print vs[1] vs[2] setvar next } { print }' cat "$destt" >"$destf" done cat "$destf" >"$dest" ac_clean "$destf" "$destt" return 0 }