Bash Templating: comment construire des fichiers de configuration à partir de templates avec Bash?
j'écris un script pour automatiser la création de fichiers de configuration pour Apache et PHP pour mon propre serveur web. Je ne veux pas utiliser D'interfaces graphiques comme CPanel ou ISPConfig.
j'ai quelques modèles de fichiers de configuration Apache et PHP. Bash script a besoin de lire des modèles, de faire de la substitution de variables et de sortir des modèles analysés dans un dossier. Quelle est la meilleure façon de le faire? Je peux penser à plusieurs égards. Laquelle est la meilleure ou peut être il y a de meilleures façons de faire qui? Je veux le faire en pur Bash (c'est facile en PHP par exemple)
1) comment remplacer ${} placeholders dans un fichier texte?
Modèle.txt:
the number is ${i}
the word is ${word}
script.sh:
#!/bin/sh
#set variables
i=1
word="dog"
#read in template one line at the time, and replace variables
#(more natural (and efficient) way, thanks to Jonathan Leffler)
while read line
do
eval echo "$line"
done < "./template.txt"
BTW, comment rediriger la sortie vers un fichier externe ici? Ai-je besoin d'échapper à quelque chose si les variables contiennent, disons, des citations?
2) utilisant le cat & sed pour remplacer chaque variable avec sa valeur:
modèle donné.txt:
The number is ${i}
The word is ${word}
commande:
cat template.txt | sed -e "s/${i}/1/" | sed -e "s/${word}/dog/"
me semble mauvais à cause de la nécessité d'échapper à beaucoup de symboles différents et avec beaucoup de variables la ligne sera trop longue.
pouvez-vous penser à une autre solution élégante et sûre?
21 réponses
vous pouvez utiliser ceci:
perl -p -i -e 's/$\{([^}]+)\}/defined $ENV{} ? $ENV{} : $&/eg' < template.txt
pour remplacer toutes les chaînes ${...}
par des variables d'environnement correspondantes (n'oubliez pas de les exporter avant d'exécuter ce script).
Pour un pur bash cela devrait fonctionner (en supposant que les variables ne contiennent pas de ${...} cordes):
#!/bin/bash
while read -r line ; do
while [[ "$line" =~ ($\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]] ; do
LHS=${BASH_REMATCH[1]}
RHS="$(eval echo "\"$LHS\"")"
line=${line//$LHS/$RHS}
done
echo "$line"
done
. Solution qui ne tient pas si RHS se réfère à une variable qui se réfère elle-même:
#!/bin/bash line="$(cat; echo -n a)" end_offset=${#line} while [[ "${line:0:$end_offset}" =~ (.*)($\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do PRE="${BASH_REMATCH[1]}" POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}" VARNAME="${BASH_REMATCH[3]}" eval 'VARVAL="$'$VARNAME'"' line="$PRE$VARVAL$POST" end_offset=${#PRE} done echo -n "${line:0:-1}"
AVERTISSEMENT : je ne connais pas un moyen de gérer correctement l'entrée avec des NULs en bash ou de conserver le montant de la fuite des retours à la ligne. Dernière variante est présentée comme elle est parce que les coquilles" amour "entrée binaire:
-
read
interprétera les tirets arrière. -
read -r
n'interprétera pas les antislashs, mais laissera tomber la dernière ligne si elle ne se termine pas par une nouvelle ligne. -
"$(…)"
va rayer autant de nouvelles lignes traînantes qu'il y en a, donc je termine…
avec; echo -n a
et utiliseecho -n "${line:0:-1}"
: Cela fait tomber le dernier caractère (qui esta
) et préserve autant de nouvelles lignes traînantes qu'il y en avait dans l'entrée (y compris no).
Essayer envsubst
FOO=foo
BAR=bar
export FOO BAR
envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF
envsubst était nouveau pour moi. Fantastique.
pour rappel, l'utilisation d'un heredoc est un excellent moyen de templater un fichier conf.
STATUS_URI="/hows-it-goin"; MONITOR_IP="10.10.2.15";
cat >/etc/apache2/conf.d/mod_status.conf <<EOF
<Location ${STATUS_URI}>
SetHandler server-status
Order deny,allow
Deny from all
Allow from ${MONITOR_IP}
</Location>
EOF
je suis d'accord avec l'utilisation de sed: c'est le meilleur outil de recherche/remplacement. Voici mon approche:
$ cat template.txt
the number is ${i}
the dog's name is ${name}
$ cat replace.sed
s/${i}/5/
s/${name}/Fido/
$ sed -f replace.sed template.txt > out.txt
$ cat out.txt
the number is 5
the dog's name is Fido
je pense que eval fonctionne vraiment bien. Il gère des gabarits avec linebreaks, espaces blancs, et toutes sortes de trucs bash. Si vous avez le contrôle total sur les modèles eux-mêmes bien sûr:
$ cat template.txt
variable1 = ${variable1}
variable2 = $variable2
my-ip = \"$(curl -s ifconfig.me)\"
$ echo $variable1
AAA
$ echo $variable2
BBB
$ eval "echo \"$(<template.txt)\"" 2> /dev/null
variable1 = AAA
variable2 = BBB
my-ip = "11.22.33.44"
cette méthode doit être utilisée avec soin, bien sûr, car eval peut exécuter du code arbitraire. Exécuter ceci comme root est à peu près hors de question. Les citations dans le modèle doivent être évitées, sinon elles seront mangées par eval
.
Vous pouvez également utiliser ici les documents si vous préférez cat
à echo
$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
@plockc provoded une solution qui évite le bash devis échapper question:
$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Edit: Retiré la partie sur l'exécution de ce en tant que root avec la commande sudo...
Edit: a ajouté un commentaire sur la façon dont les citations doivent être échappées, a ajouté la solution de plockc au mélange!
Edit Jan 6, 2017
j'avais besoin de conserver les doubles guillemets dans mon fichier de configuration, donc le double échappant aux doubles guillemets avec les aides de sed:
render_template() {
eval "echo \"$(sed 's/\"/\\"/g' )\""
}
Je ne peux pas penser à suivre de nouvelles lignes, mais des lignes vides entre les deux sont gardées.
bien que ce soit un vieux sujet, IMO j'ai trouvé une solution plus élégante ici: http://pempek.net/articles/2013/07/08/bash-sh-as-template-engine/
#!/bin/sh
# render a template configuration file
# expand variables + preserve formatting
render_template() {
eval "echo \"$(cat )\""
}
user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file
Tous les crédits Grégory Pakosz .
j'ai une solution de bash comme mogsie mais avec heredoc au lieu de herestring pour vous permettre d'éviter d'échapper aux doubles citations
eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
une version plus longue mais plus robuste de la réponse acceptée:
perl -pe 's;(\*)($([a-zA-Z_][a-zA-Z_0-9]*)|$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr(,0,int(length()/2)).(&&length()%2?:$ENV{||});eg' template.txt
ceci étend toutes les instances de $VAR
ou ${VAR}
à leurs valeurs d'environnement (ou, si elles ne sont pas définies, la chaîne vide).
il échappe correctement aux antislashs, et accepte un $ pour inhiber la substitution (contrairement à envsubst, qui, il s'avère, ne fait pas cela ).
donc, si votre environnement est:
FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi
et votre modèle est:
Two ${TARGET} walk into a \$FOO. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
$BAZ replies, "${NOPE}s."
le résultat serait:
Two backslashes walk into a \bar. \
$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."
si vous voulez seulement échapper à backslashs avant $ (vous pouvez écrire "C:\Windows\System32" dans un modèle inchangé), utilisez cette version légèrement modifiée:
perl -pe 's;(\*)($([a-zA-Z_][a-zA-Z_0-9]*)|$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr(,0,int(length()/2)).(length()%2?:$ENV{||});eg' template.txt
Je l'aurais fait de cette façon, probablement moins efficace, mais plus facile à lire/entretenir.
TEMPLATE='/path/to/template.file'
OUTPUT='/path/to/output.file'
while read LINE; do
echo $LINE |
sed 's/VARONE/NEWVALA/g' |
sed 's/VARTWO/NEWVALB/g' |
sed 's/VARTHR/NEWVALC/g' >> $OUTPUT
done < $TEMPLATE
prenant la réponse de ZyX en utilisant pur bash mais avec un nouveau style regex matching et la substitution de paramètre indirecte il devient:
#!/bin/bash
regex='$\{([a-zA-Z_][a-zA-Z_0-9]*)\}'
while read line; do
while [[ "$line" =~ $regex ]]; do
param="${BASH_REMATCH[1]}"
line=${line//${BASH_REMATCH[0]}/${!param}}
done
echo $line
done
Si l'aide de Perl est une option et que vous êtes content avec le fondant des expansions sur environnement variables (par opposition à tous les shell variables), prendre en considération Stuart P. Bentley robuste réponse .
Cette réponse vise à fournir un bash seule solution que - malgré l'utilisation d' eval
- doit être sûr à utiliser .
Le objectifs sont:
- l'Appui de l'expansion de "l'151940920" et
$name
les références à des variables. - empêcher toute autre expansion:
- commandes de substitution (
$(...)
et syntaxe héritée`...`
) - substitutions arithmétiques (
$((...))
et legs la syntaxe$[...]
).
- commandes de substitution (
- permet la suppression sélective de l'expansion variable en préfixant
\
(${name}
). - préserver les bocaux Spéciaux. dans les instances d'entrée, notamment
"
et\
. - permet l'entrée soit via les arguments, soit via stdin.
fonction expandVars()
:
expandVars() {
local txtToEval=$* txtToEvalEscaped
# If no arguments were passed, process stdin input.
(( $# == 0 )) && IFS= read -r -d '' txtToEval
# Disable command substitutions and arithmetic expansions to prevent execution
# of arbitrary commands.
# Note that selectively allowing $((...)) or $[...] to enable arithmetic
# expressions is NOT safe, because command substitutions could be embedded in them.
# If you fully trust or control the input, you can remove the `tr` calls below
IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '')
# Pass the string to `eval`, escaping embedded double quotes first.
# `printf %s` ensures that the string is printed without interpretation
# (after processing by by bash).
# The `tr` command reconverts the previously escaped chars. back to their
# literal original.
eval printf %s "\"${txtToEvalEscaped//\"/\\"}\"" | tr '' '`(['
}
exemples:
$ expandVars '$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls) # only $HOME was expanded
$ printf '$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
- pour des raisons de performance, la fonction lit entrée stdin tout d'un coup dans la mémoire, mais il est facile d'adapter la fonction à une approche ligne par ligne.
- supporte aussi Non-basic extensions variables telles que
${HOME:0:10}
, à condition qu'elles ne contiennent aucune commande intégrée ou substitutions arithmétiques, telles que${HOME:0:$(echo 10)}
- de telles substitutions incrustées cassent réellement la fonction (parce que toutes les instances
$(
et`
sont aveuglément échappées). - de même, les références variables mal formées telles que
${HOME
(fermeture manquante}
) cassent la fonction.
- de telles substitutions incrustées cassent réellement la fonction (parce que toutes les instances
- en raison de la manipulation par bash de cordes à double citation, les jeux de barres obliques sont traités comme suit::
-
$name
empêche l'expansion. - un simple
\
non suivi de$
est conservé tel quel. - Si vous voulez représenter adjacentes multiples
\
cas, vous devez d' double ; par exemple:-
\
->\
- la même chose que juste\
-
\\
->\
-
- l'entrée ne doit pas contenir les caractères suivants (rarement utilisés), qui sont utilisés à des fins internes:
0x1
,0x2
,0x3
.
-
- il y a une préoccupation largement hypothétique que si bash devait introduire une nouvelle syntaxe d'extension, cette fonction ne pourrait pas empêcher de telles extensions - voir ci-dessous pour une solution qui n'utilise pas
eval
.
si vous cherchez une solution plus restrictive que seulement supporte ${name}
expansions - i.e., avec obligatoire broches bouclées, ignorant $name
références - Voir cette réponse de la mienne.
Voici une version améliorée de la bash-seulement, eval
- solution libre de la réponse acceptée :
les améliorations sont:
- Soutien à l'expansion des deux
${name}
et$name
les références à des variables. - Support pour
\
- échappant aux références variables qui ne devraient pas être étendues. - contrairement à la solution basée sur
eval
ci-dessus,- non basiques les expansions sont ignorées
- les références à des variables mal formées sont ignorées (elles ne cassent pas le script)
IFS= read -d '' -r lines # read all input from stdin at once
end_offset=${#lines}
while [[ "${lines:0:end_offset}" =~ (.*)$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
pre=${BASH_REMATCH[1]} # everything before the var. reference
post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
# extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
[[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
# Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
if [[ $pre =~ \+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
: # no change to $lines, leave escaped var. ref. untouched
else # replace the variable reference with the variable's value using indirect expansion
lines=${pre}${!varName}${post}
fi
end_offset=${#pre}
done
printf %s "$lines"
cette page décrit une réponse avec awk
awk '{while(match("151900920","[$]{[^}]*}")) {var=substr("151900920",RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
Parfait shtpl . (projet de mine, de sorte qu'il n'est pas largement utilisé et manque de documentation. Mais voici la solution qu'il offre de toute façon. Peut vous voulez le tester.)
exécutez Simplement:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
résultat:
the number is 1
the word is dog
amusez-vous bien.
Voici une autre solution de bash pure:
- il utilise heredoc, donc:
- la complexité n'augmente pas à cause de la syntaxe supplémentaire requise Le modèle
- peut inclure le code bash
- qui vous permet également d'indenter des trucs correctement. Voir ci-dessous.
- il n'utilise pas eval, donc:
- pas de problèmes avec le rendu des lignes vides de fuite
- pas de problèmes avec les guillemets dans le modèle
$ cat code
#!/bin/bash
LISTING=$( ls )
cat_template() {
echo "cat << EOT"
cat ""
echo EOT
}
cat_template template | LISTING="$LISTING" bash
$ cat template
(avec des nouvelles et des guillemets)
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
$( echo "$LISTING" | sed 's/^/ /' )
<pre>
</p>
</body>
</html>
sortie
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
code
template
<pre>
</p>
</body>
</html>
Voici une autre solution: générer un script bash avec toutes les variables et le contenu du fichier template, ce script ressemblerait à ceci:
word=dog
i=1
cat << EOF
the number is ${i}
the word is ${word}
EOF
si nous insérons ce script dans bash il produira la sortie désirée:
the number is 1
the word is dog
voici comment générer ce script et l'alimenter en bash:
(
# Variables
echo word=dog
echo i=1
# add the template
echo "cat << EOF"
cat template.txt
echo EOF
) | bash
Discussion
- les parenthèses ouvre un sous-shell, son but est de regrouper toutes les sorties générées
- dans le sous-shell, nous générons toutes les déclarations variables
- également dans le sous-shell, nous générons la commande
cat
avec HEREDOC - enfin, nous alimentons la sortie sub shell à bash et produisons la sortie désirée
-
Si vous voulez rediriger cette sortie dans un fichier, remplacez la dernière ligne avec:
) | bash > output.txt
Vous pouvez également utiliser bashible (qui utilise en interne l'évaluation de l'approche décrite ci-dessus/ci-dessous).
il y a un exemple, comment générer un HTML à partir de plusieurs parties:
https://github.com/mig1984/bashible/tree/master/examples/templates
# Usage: template your_file.conf.template > your_file.conf
template() {
local IFS line
while IFS=$'\n\r' read -r line ; do
line=${line//\/\\} # escape backslashes
line=${line//\"/\\"} # escape "
line=${line//\`/\\`} # escape `
line=${line//$/\$} # escape $
line=${line//\${/${} # de-escape ${ - allows variable substitution: ${var} ${var:-default_value} etc
# to allow arithmetic expansion or command substitution uncomment one of following lines:
# line=${line//\$\(/$\(} # de-escape $( and $(( - allows $(( 1 + 2 )) or $( command ) - UNSECURE
# line=${line//\$\(\(/$\(\(} # de-escape $(( - allows $(( 1 + 2 ))
eval "echo \"${line}\"";
done < ""
}
c'est la fonction de bash pure réglable à votre goût, utilisé dans la production et ne doit pas casser sur n'importe quel input. En cas de casse - laissez-moi savoir.
Voici une fonction bash qui préserve l'espace:
# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
while IFS='' read line; do
eval echo \""${line}"\"
done < ""
}
voici un script modifié perl
basé sur quelques-unes des autres réponses:
perl -pe 's/([^\]|^)$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/.$ENV{}/eg' -i template
Caractéristiques (basé sur mes besoins, mais devrait être facile à modifier):
- ne permet pas d'augmenter les paramètres (par exemple \${VAR}).
- supporte les extensions de paramètres de la forme ${VAR}, mais pas $VAR.
- remplace ${VAR} par une chaîne vierge s'il n'y a pas de var envar.
- seulement prend en charge a-z, A-Z, 0-9 et les caractères de soulignement dans le nom (à l'exclusion des chiffres dans la première position).
au lieu de réinventer la roue aller avec envsubst Peut être utilisé dans presque tous les scénarios, par exemple la construction de fichiers de configuration à partir de variables d'environnement dans des conteneurs docker.
Si sur mac assurez-vous d'avoir homebrew puis le lien de gettext:
brew install gettext
brew link --force gettext
./modèle.cfg
# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}
./.env:
SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2
. /configure.sh
#!/bin/bash
cat template.cfg | envsubst > whatever.cfg
maintenant il suffit de l'utiliser:
# make script executable
chmod +x ./configure.sh
# source your variables
. .env
# export your variables
# In practice you may not have to manually export variables
# if your solution dependins on tools that utilise .env file
# automatically like pipenv etc.
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh