nutools/lib/ulib/template

628 lines
23 KiB
Bash

##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## Gestion de fichiers modèles
##@cooked nocomments
##@require base
uprovide template
urequire base
function __template_prel_abspath() {
# afficher le chemin absolu du fichier $1. Si $1 est un chemin relatif, le
# répertoire de référence pour le calcul du chemin absolu dépend du
# répertoire courant: si l'on est dans un des sous-répertoires du répertoire
# de destination $2, calculer le chemin absolu par rapport au répertoire
# courant. Sinon, l'exprimer par rapport à $2.
if withinpath "$2" "$(pwd)"; then
abspath "$1"
else
abspath "$1" "$2"
fi
}
function __template_check_srcdir() {
[ -n "$1" ] || {
eerror "Vous devez spécifier le répertoire source"
return 1
}
[ -d "$1" ] || {
eerror "$1: répertoire source introuvable"
return 1
}
return 0
}
function __template_check_destdir() {
[ -n "$1" ] || {
eerror "Vous devez spécifier le répertoire de destination"
return 1
}
[ -d "$1" ] || {
eerror "$1: répertoire destination introuvable"
return 1
}
return 0
}
function __template_plsort() {
# Trier par taille du chemin
awk '{print length($0) ":" $0}' |
sort -n "$@" |
awk '{sub(/[0-9]+:/, ""); print}'
}
function __template_search_destdir() {
# Chercher à partir du répertoire courant si un des répertoires parents
# s'appelle $1. Retourner vrai si le répertoire a été trouvé.
local dir="$(pwd)" dirname
while true; do
setx dirname=basename -- "$dir"
if [ "$dir" == / ]; then
# s'arrêter à la racine
return 1
elif [ "$dir" == "$HOME" ]; then
# s'arrêter au répertoire HOME
return 1
elif [ "$dirname" == "$1" ]; then
echo "$dir"
return 0
elif [ "$dirname" == ".$1" ]; then
echo "$dir"
return 0
fi
setx dir=dirname -- "$dir"
done
}
function __template_set_destdir() {
local __destdir="${1:-destdir}" __autocreate="${2:-autocreate}" __name="${3:-template}"
setv "$__autocreate"
if [ -z "${!__destdir}" ]; then
if [ -d "$__name" ]; then
setv "$__destdir" "$__name"
elif [ -d ".$__name" ]; then
setv "$__destdir" ".$__name"
elif setx "$__destdir" __template_search_destdir "$__name"; then
estepn "Sélection automatique de $(ppath "${!__destdir}")"
elif [ -e "$__name" ]; then
eerror "Vous devez spécifier le répertoire de destination avec -d"
return 1
else
setv "$__destdir" "$__name"
setv "$__autocreate" 1
fi
fi
setx "$__destdir" abspath "${!__destdir}"
return 0
}
function __template_set_var() {
# Mettre à jour la variable $1 avec la valeur $2 en tenant compte de
# certaines dépendances. Par exemple, si on modifie host, il faut mettre à
# jour hostname.
# La variable __TEMPLATE_DEFAULTF_var contient le nom d'une fonction qui
# retourne la valeur par défaut de la variable. Cette fonction est appelée
# avec le nom de la variable comme premier argument.
# La variable __TEMPLATE_UPDATEF_var contient le nom d'une fonction qui met
# à jour les variables dépendantes. Cette fonction est appelée avec le nom
# de la variable comme premier argument et sa nouvelle valeur en deuxième
# argument.
# Retourner vrai si la valeur a été changée
local __orig_value="${!1}"
local __defaultf="__TEMPLATE_DEFAULTF_$1" __updatef="__TEMPLATE_UPDATEF_$1"
array_contains TEMPLATE_DYNAMIC_VARS "$1" || array_addu TEMPLATE_STATIC_VARS "$1"
[ "$3" == writable ] && array_del TEMPLATE_NOWRITE_VARS
[ -z "$2" -a -n "${!__defaultf}" ] && set -- "$1" "$("${!__defaultf}" "$1")"
setv "$1" "$2"
[ -n "${!__updatef}" ] && "${!__updatef}" "$1" "$2"
[ "$2" != "$__orig_value" ]
}
# liste des variables qu'il faut remplacer dans les fichiers sources
TEMPLATE_STATIC_VARS=()
function __template_setup_tmpfile() {
if [ ${#TEMPLATE_STATIC_VARS[*]} -gt 0 ]; then
# S'il y a des variables à remplacer, préparer un fichier temporaire
ac_set_tmpfile tmpfile
fi
}
function __template_clean_tmpfile() {
if [ ${#TEMPLATE_STATIC_VARS[*]} -gt 0 ]; then
ac_clean "$tmpfile"
fi
}
function __template_fillvars() {
# Pour chacune des variables VAR de TEMPLATE_STATIC_VARS, remplacer dans le
# fichier $1 les occurences de @@VAR@@ par la valeur $VAR
# Afficher le chemin vers le fichier temporaire qui contient le fichier
# modifié. Si $2 est spécifié, prendre ce chemin comme fichier temporaire.
# Sinon, créer un nouveau fichier temporaire. Si le fichier ne contient
# aucune occurence de variable, afficher le chemin original $1.
[ ${#TEMPLATE_STATIC_VARS[*]} -eq 0 ] && { echo "$1"; return; }
# chercher si le fichier contient au moins un des tags de
# TEMPLATE_STATIC_VARS
local __var __tag __found
for __var in "${TEMPLATE_STATIC_VARS[@]}"; do
__tag="@@${__var}@@"
if quietgrep "$__tag" "$1"; then
__found=1
break
fi
done
[ -n "$__found" ] || { echo "$1"; return; }
# construire le script sed pour le remplacement des variables
local __script __first=1 __repl
for __var in "${TEMPLATE_STATIC_VARS[@]}"; do
[ -n "$__first" ] || __script="$__script"$'\n'
__first=
__script="${__script}s/@@${__var}@@/$(qseds "${!__var}")/g"
done
sed "$__script" <"$1" >"$2"
echo "$2"
}
function template_list() {
# Soit $N le séparateur --, lister les fichiers des répertoires sources
# $2..$(N-1) qui seraient fusionnés avec template_merge() ou supprimés avec
# template_unmerge() du répertoire destination $1. Si des chemins sont spécifiés
# avec les arguments $(N+1)..@, ne traiter que les fichiers qui correspondent à
# ces spécifications. Exemple:
# template_list destdir srcdirs... -- specs...
local destdir="${1:-.}"; shift
__template_check_destdir "$destdir" || return 1
setx destdir=abspath "$destdir"
local -a srcdirs; local srcdir
while [ $# -gt 0 ]; do
srcdir="$1"; shift
[ "$srcdir" == -- ] && break
__template_check_srcdir "$srcdir" || return 1
setx srcdir=abspath "$srcdir"
array_add srcdirs "$srcdir"
done
local rel2pwd
withinpath "$destdir" "$(pwd)" && rel2pwd=1
if [ $# -eq 0 ]; then
[ -n "$rel2pwd" ] && set -- . || set -- "$destdir"
fi
local tmpfile; __template_setup_tmpfile
local spec srcspec src content dest list
local -a srcfiles
for spec in "$@"; do
setx srcspec=__template_prel_abspath "$spec" "$destdir"
withinpath "$destdir" "$srcspec" || continue
srcspec="${srcspec#$destdir}"
for srcdir in "${srcdirs[@]}"; do
[ -e "$srcdir$srcspec" ] || continue
array_from_lines srcfiles "$(find "$srcdir$srcspec" -type f)"
for src in "${srcfiles[@]}"; do
setx content=__template_fillvars "$src" "$tmpfile"
dest="$destdir/${src#$srcdir/}"
list=
if [ -L "$dest" ]; then
:
elif [ -f "$dest" ]; then
testsame "$content" "$dest" && list=1
else
list=1
fi
[ -n "$list" ] || continue
if [ -n "$rel2pwd" ]; then
relpath "$dest"
else
echo "${src#$srcdir/}"
fi
done
done
done | csort -u
__template_clean_tmpfile
}
function template_merge() {
# Soit $N le séparateur --, copier dans le répertoire destination $1 les
# fichiers des répertoires sources $2..$(N-1) correspondant aux spécifications
# $(N+1)..@, si ces fichiers n'ont pas été modifiés dans le répertoire de
# destination.
# Les fichiers sources ayant l'extension .template sont ignorés par défaut, sauf
# s'ils sonts demandés explicitement. Exemple:
# template_merge destdir srcdirs... -- specs...
local destdir="${1:-.}"; shift
__template_check_destdir "$destdir" || return 1
setx destdir=abspath "$destdir"
local -a srcdirs; local srcdir
while [ $# -gt 0 ]; do
srcdir="$1"; shift
[ "$srcdir" == -- ] && break
__template_check_srcdir "$srcdir" || return 1
setx srcdir=abspath "$srcdir"
array_add srcdirs "$srcdir"
done
[ $# -gt 0 ] || set -- "$destdir"
local tmpfile; __template_setup_tmpfile
local spec template srcspec src content dest
local srcfiles
for spec in "$@"; do
setb template=[ "${spec%.template}" != "$spec" ]
setx srcspec=__template_prel_abspath "$spec"
if ! withinpath "$destdir" "$srcspec"; then
ewarn "$spec: fichier ignoré"
continue
fi
srcspec="${srcspec#$destdir}"
for srcdir in "${srcdirs[@]}"; do
if [ -z "$template" -a ! -e "$srcdir$srcspec" -a -e "$srcdir$srcspec.template" ]; then
srcspec="$srcspec.template"
template=1
fi
[ -e "$srcdir$srcspec" ] || continue
ebegin "$(basename -- "$srcdir") --> $(ppath "$destdir$srcspec")"
s=0
if [ -n "$template" ]; then
array_from_lines srcfiles "$(find "$srcdir$srcspec" -type f)"
else
array_from_lines srcfiles "$(find "$srcdir$srcspec" -type f | grep -v '\.template$')"
fi
for src in "${srcfiles[@]}"; do
setx content=__template_fillvars "$src" "$tmpfile"
dest="$destdir/${src#$srcdir/}"
[ -n "$template" ] && dest="${dest%.template}"
if [ -L "$dest" ]; then
edotw 0 "LOCALLY MODIFIED: $(ppath "$dest")"
elif [ -f "$dest" ]; then
if testsame "$content" "$dest"; then
show_debug && edot 0 "ALREADY COPIED: $(ppath "$dest")"
else
edotw 0 "LOCALLY MODIFIED: $(ppath "$dest")"
fi
else
mkdirof "$dest"
cp "$content" "$dest"; r=$?
edot $r "COPY: $(ppath "$dest")"
[ $r -eq 0 ] || s=$r
fi
done
eend $s
done
done
__template_clean_tmpfile
}
function template_unmerge() {
# Soit $N le séparateur --, supprimer du répertoire destination $1 les fichiers
# provenant des répertoires sources $2..$(N-1) et qui n'ont pas été modifiés. Si
# des chemins sont spécifiés avec les arguments $(N+1)..@, ne traiter que les
# fichiers qui correspondent à ces spécifications. Exemple:
# template_unmerge destdir srcdirs... -- specs...
local destdir="${1:-.}"; shift
__template_check_destdir "$destdir" || return 1
setx destdir=abspath "$destdir"
local -a srcdirs; local srcdir
while [ $# -gt 0 ]; do
srcdir="$1"; shift
[ "$srcdir" == -- ] && break
__template_check_srcdir "$srcdir" || return 1
setx srcdir=abspath "$srcdir"
array_add srcdirs "$srcdir"
done
[ $# -gt 0 ] || set -- "$destdir"
local tmpfile; __template_setup_tmpfile
local spec srcspec src content dest
local srcfiles
for spec in "$@"; do
setx srcspec=__template_prel_abspath "$spec"
if ! withinpath "$destdir" "$srcspec"; then
ewarn "$spec: fichier ignoré"
continue
fi
srcspec="${srcspec#$destdir}"
for srcdir in "${srcdirs[@]}"; do
[ -e "$srcdir$srcspec" ] || continue
s=0
ebegin "$(ppath "$destdir$srcspec")"
array_from_lines files "$(find "$srcdir$srcspec" -type f)"
for src in "${files[@]}"; do
setx content=__template_fillvars "$src" "$tmpfile"
dest="$destdir/${src#$srcdir/}"
if [ -L "$dest" ]; then
edotw 0 "LOCALLY MODIFIED: $(ppath "$dest")"
elif [ -f "$dest" ]; then
if testsame "$content" "$dest"; then
rm -f "$dest"; r=$?
edot $r "REMOVE: $(ppath "$dest")"
[ $r -eq 0 ] || s=$r
else
edotw 0 "LOCALLY MODIFIED: $(ppath "$dest")"
fi
else
show_debug && edot 0 "ALREADY REMOVED: $(ppath "$dest")"
fi
done
eend $s
done
done
__template_clean_tmpfile
}
function template_cleandest() {
# Supprimer dans le répertoire de destination $1 tous les répertoires vides.
# Cette fonction est habituellement utilisée après template_unmerge()
# Ignorer les chemins qui contiennent .git/ et .svn/
local -a dirs
[ -d "$1" ] || return 1
array_from_lines dirs "$(cd "$1"; find . -type d | grep -v .git/ | grep -v .svn/ | __template_plsort -r)"
array_del dirs .
(cd "$1"; rmdir "${dirs[@]}" 2>/dev/null)
}
function template_diff() {
# Afficher les différences entre les fichiers du répertoire de destination $1 et
# les fichiers des répertoires sources $2..@
local destdir="${1:-.}"; shift
__template_check_destdir "$destdir" || return 1
setx destdir=abspath "$destdir"
local -a srcdirs; local srcdir
while [ $# -gt 0 ]; do
srcdir="$1"; shift
[ "$srcdir" == -- ] && break
__template_check_srcdir "$srcdir" || return 1
setx srcdir=abspath "$srcdir"
array_add srcdirs "$srcdir"
done
local tmpfile; __template_setup_tmpfile
local src content dest
local -a srcfiles
for srcdir in "${srcdirs[@]}"; do
array_from_lines srcfiles "$(find "$srcdir" -type f)"
for src in "${srcfiles[@]}"; do
setx content=__template_fillvars "$src" "$tmpfile"
dest="$destdir/${src#$srcdir/}"
if [ -f "$dest" ] && testdiff "$content" "$dest"; then
diff -uwB "$content" "$dest"
fi
done
done | page_maybe -S
__template_clean_tmpfile
}
function template_srcdir() {
# Obtenir le chemin vers le répertoire source de templates $1, situé dans
# ULIBDIR/templates
urequire ulib
if [ -n "$ULIBDIR" ]; then
echo "$ULIBDIR/templates/$1"
else
abspath "templates/$1"
fi
}
function templatectl_config() {
# Obtenir le chemin vers le fichier de configuration pour le répertoire $1
# Si $2==nohideconfig, utiliser le nom CONFIG.conf, sinon utiliser par défaut
# .CONFIG sauf si le fichier CONFIG.conf existe
local config="$(basename -- "$TEMPLATECTL_CONFIG")"
if [ -f "$1/$config.conf" -o "$2" == nohideconfig ]; then
echo "$1/$config.conf"
else
echo "$1/.$config"
fi
}
function templatectl_loadvars() {
# Charger les valeurs des variables depuis le fichier $1
# Les variables suivantes doivent être définies:
# - Le tableau TEMPLATECTL_DEFAULTS permet de donner une valeur par défaut aux
# variables mentionnées dans TEMPLATE_STATIC_VARS. C'est une liste de valeurs
# de la forme 'name=value'
# - Le tableau TEMPLATECTL_VARS contient des variables supplémentaires
# spécifiées par l'utilisateur. C'est une liste de valeurs de la forme
# 'name=value'
# - TEMPLATE_STATIC_VARS doit contenir une liste de noms de variables qui
# peuvent être remplacés dans les fichiers de template.
# - TEMPLATE_DYNAMIC_VARS contient une liste de noms de variables valides, mais
# qui ne doivent pas être remplacés, en effet, ils sont utilisés pour le
# déploiement des fichiers.
# - TEMPLATE_NOWRITE_VARS contient une liste de noms de variables qui ne
# devraient pas être écrits dans le fichier des variables, sauf si elles
# reçoivent une valeur explicite de la part de l'utilisateur. Ce tableau est
# mis à jour lors de l'analyse du tableau TEMPLATECTL_VARS
local -a __template_vars __dynamic_vars
local __t_var __t_name __t_value
array_copy __template_vars TEMPLATECTL_DEFAULTS
configdir="$(dirname -- "$1")"
[ -f "$1" ] && source "$1"
for __t_var in "${__template_vars[@]}"; do
splitvar "$__t_var" __t_name __t_value
__template_set_var "$__t_name" "$__t_value"
done
array_contains TEMPLATE_STATIC_VARS configdir && __template_set_var configdir "$configdir"
for __t_var in "${__dynamic_vars[@]}"; do
splitvar "$__t_var" __t_name __t_value
array_del TEMPLATE_STATIC_VARS "$__t_name"
array_addu TEMPLATE_DYNAMIC_VARS "$__t_name"
__template_set_var "$__t_name" "$__t_value"
done
array_contains TEMPLATE_DYNAMIC_VARS configdir && __template_set_var configdir "$configdir"
local __t_modified=1
for __t_var in "${TEMPLATECTL_VARS[@]}"; do
splitvar "$__t_var" __t_name __t_value
__template_set_var "$__t_name" "$__t_value" writable && __t_modified=0
done
return $__t_modified
}
function templatectl_writevars() {
# Ecrire les variables dans le fichier $1
local __t_var
echo "# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" >"$1"
if [ ${#TEMPLATE_DYNAMIC_VARS[*]} -gt 0 ]; then
echo "__dynamic_vars=(" >>"$1"
for __t_var in "${TEMPLATE_DYNAMIC_VARS[@]}"; do
array_contains TEMPLATE_NOWRITE_VARS "$__t_var" && continue
echo_setv "$__t_var=${!__t_var}" >>"$1"
done
echo ")" >>"$1"
fi
echo "__template_vars=(# ne pas modifier" >>"$1"
for __t_var in "${TEMPLATE_STATIC_VARS[@]}"; do
array_contains TEMPLATE_NOWRITE_VARS "$__t_var" && continue
echo_setv "$__t_var=${!__t_var}" >>"$1"
done
echo ")" >>"$1"
}
function templatectl_list_vars() {
# Afficher les valeurs des variables
local __var
echo "# template vars"
for __var in "${TEMPLATE_STATIC_VARS[@]}"; do
echo_setv "$__var=${!__var}"
done
if [ ${#TEMPLATE_DYNAMIC_VARS[*]} -gt 0 ]; then
echo "# dynamic vars"
for __var in "${TEMPLATE_DYNAMIC_VARS[@]}"; do
echo_setv "$__var=${!__var}"
done
fi
}
__TEMPLATECTL_HELP="\
-d, --destdir DESTDIR
Spécifier le répertoire local dans lequel copier les fichiers templates
-s, --srcdir SRCDIR
Ajouter un répertoire à la liste des répertoires sources pour les templates.
Si ce n'est pas un chemin, c'est le nom d'un répertoire dans le répertoires
des templates par défaut.
-l, --list
Lister les templates disponibles.
-m, --merge
Copier les templates spécifiés dans le répertoire local s'il n'y existent
pas déjà. Les templates ayant l'extension '.template' doivent être demandés
explicitement. Sinon, ils sont ignorés.
-z, --unmerge
Supprimer les fichiers du répertoire local s'ils n'ont pas été modifiés par
rapport aux templates.
-C, --clean
Supprimer les répertoires vides dans le répertoire local. Peut être utile
après -z
-g, --diff
Afficher les différences entre les templates et les fichiers du répertoire
local.
-L, --list-vars
Afficher pour information les valeurs par défaut des variables de template.
-v, --var NAME=VALUE
Spécifier la valeur d'une variable. Toutes les variables sont autorisées,
sauf celles qui figurent dans la liste des variables dynamiques.
-w, --write-vars
Ecrire dans le fichier $TEMPLATECTL_CONFIG les valeurs des variables, ce qui
permet après édition du fichier d'éviter de les spécifier à chaque fois avec
l'option -v
Le fichier n'est pas écrasé s'il existe déjà."
function __display_templatectl_help() { uecho "$__TEMPLATECTL_HELP"; }
function templatectl() {
# Fonction de haut niveau qui facilite l'utilisation des fonctions template_*
# définir la fonction __display_templatectl_help() pour l'affichage de l'aide
# - Le tableau TEMPLATECTL_SRCDIRS doit contenir la liste des répertoires
# sources pour les templates. Alternativement, il est possible de définir la
# variable TEMPLATECTL_SRCDIR s'il n'y a qu'un seul répertoire source pour le
# template
# - TEMPLATECTL_CONFIG est le nom de base du fichier à partir duquel sont
# chargées les variables et dans lequel sont écrites les variables avec
# l'option --write-vars
# Si le nom de base est CONFIG, le fichier s'appelera .CONFIG si l'option
# --hide-config est utilisée (par défaut) ou CONFIG.conf si l'option
# --no-hide-config est utilisée
# Les variables de template_loadvars() sont aussi prises en compte
local -a __tc_srcdirs; local __tc_srcdir
if [ ${#TEMPLATECTL_SRCDIRS[*]} -gt 0 ]; then
:
elif [ -n "$TEMPLATECTL_SRCDIR" ]; then
local -a TEMPLATECTL_SRCDIRS
TEMPLATECTL_SRCDIRS=("$TEMPLATECTL_SRCDIR")
fi
for __tc_srcdir in "${TEMPLATECTL_SRCDIRS[@]}"; do
if ! withpath "$__tc_srcdir"; then
__tc_srcdir="$(template_srcdir "$__tc_srcdir")"
fi
array_add __tc_srcdirs "$__tc_srcdir"
done
local -a __tc_args
local __tc_auto=1 __tc_load_vars=1
local __tc_destdir __tc_config __tc_nohideconfig
local __tc_list __tc_merge __tc_unmerge __tc_clean __tc_diff __tc_list_vars __tc_write_vars
parse_opts "${PRETTYOPTS[@]}" \
--help '$exit_with __display_templatectl_help' \
-d:,--destdir: __tc_destdir= \
-s:,--srcdir: __tc_srcdirs \
--config: __tc_config= \
--no-hide-config __tc_nohideconfig=1 \
--hide-config __tc_nohideconfig= \
--load-vars __tc_load_vars=1 \
--no-load-vars __tc_load_vars= \
--noop __tc_auto= \
-l,--list '$__tc_list=1; __tc_auto=' \
-m,--merge '$__tc_merge=1; __tc_auto=' \
-z,--unmerge '$__tc_unmerge=1; __tc_auto=' \
-C,--clean '$__tc_clean=1; __tc_auto=' \
-g,--diff '$__tc_diff=1; __tc_auto=' \
-L,--list-vars '$__tc_list_vars=1; __tc_auto=' \
-v:,--var: TEMPLATECTL_VARS \
-w,--write-vars __tc_write_vars=1 \
@ __tc_args -- "$@" && set -- "${__tc_args[@]}" || { eerror "$__tc_args"; return 1; }
[ -n "$__tc_destdir" ] || __tc_destdir=.
__template_check_destdir "$__tc_destdir" || return 1
array_isempty __tc_srcdirs && return 1
for __tc_srcdir in "${__tc_srcdirs[@]}"; do
__template_check_srcdir "$__tc_srcdir" || return 1
done
[ -n "$__tc_config" ] || __tc_config="$(templatectl_config "$destdir" ${__tc_nohideconfig:+nohideconfig})"
if [ -n "$__tc_load_vars" ]; then
templatectl_loadvars "$__tc_config"
fi
if [ -n "$__tc_write_vars" ]; then
if [ -f "$__tc_config" ]; then
ewarn "Refus d'écraser le fichier existant $(ppath "$__tc_config")"
else
templatectl_writevars "$__tc_config"
fi
fi
[ -n "$__tc_auto" ] && __tc_list=1
[ -n "$__tc_list_vars" ] && templatectl_list_vars
[ -n "$__tc_list" ] && template_list "$__tc_destdir" "${__tc_srcdirs[@]}" -- "$@"
[ -n "$__tc_merge" ] && template_merge "$__tc_destdir" "${__tc_srcdirs[@]}" -- "$@"
[ -n "$__tc_unmerge" ] && template_unmerge "$__tc_destdir" "${__tc_srcdirs[@]}" -- "$@"
[ -n "$__tc_clean" ] && template_cleandest "$__tc_destdir" "$@"
[ -n "$__tc_diff" ] && template_diff "$__tc_destdir" "${__tc_srcdirs[@]}" -- "$@"
}