web-dev-qa-db-fra.com

Bash Templating: Comment construire des fichiers de configuration à partir de modèles 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 souhaite utiliser aucune interface graphique comme CPanel ou ISPConfig.

J'ai quelques modèles d'Apache et des fichiers de configuration PHP. Le script Bash doit lire les modèles, créer des substitutions de variables et afficher les modèles analysés dans un dossier. Quel est le meilleur moyen de le faire? Je pense Je veux le faire en pur Bash (c’est facile en PHP par exemple)).

1) Comment remplacer $ {} espaces réservés dans un fichier texte?

template.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 puis-je rediriger la sortie vers un fichier externe ici? Dois-je échapper quelque chose si les variables contiennent, par exemple, des citations?

2) Utiliser cat & sed pour remplacer chaque variable par sa valeur:

Étant donné template.txt:

The number is ${i}
The Word is ${Word}

Commander:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${Word}/dog/"

Cela me semble mauvais en raison 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?

109

Vous pouvez utiliser ceci:

Perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt

pour remplacer toutes les chaînes ${...} par les variables d'environnement correspondantes (n'oubliez pas de les exporter avant d'exécuter ce script).

Pour pur bash, cela devrait fonctionner (en supposant que les variables ne contiennent pas de chaînes $ {...}):

#!/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 se bloque pas si RHS fait référence à une variable qui fait référence à elle-même:

 #!/bin/bash 
 line = "$ (cat; echo -na)" 
 end_offset = $ {# line} 
 tant que [["$ {ligne: 0: $ end_offset} "= ~ (. *) (\ $\{([a-zA-Z _] [a-zA-Z_0-9] *) \}) (. *)]; do 
 PREF = "$ {BASH_REMATCH [1]}" 
 POST = "$ {BASH_REMATCH [4]} $ {ligne: $ end_offset: $ {# ligne}}" ".____. ] VARNAME = "$ {BASH_REMATCH [3]}" 
 Eval 'VARVAL = "$' $ VARNAME" "'
 Line =" $ PRE $ VARVAL $ POST "
 End_offset = $ {# PRE} 
 Terminé 
 Echo -n "$ {ligne: 0: -1}" 

WARNING : Je ne connais pas le moyen de gérer correctement les entrées avec NUL dans bash ou de préserver la quantité de nouvelles lignes. La dernière variante est présentée telle quelle car les coquilles "adorent" les entrées binaires:

  1. read interprétera les barres obliques inverses.
  2. read -r N'interprétera pas les barres obliques inverses, mais supprimera la dernière ligne si elle ne se termine pas par une nouvelle ligne.
  3. "$(…)" supprimera autant de nouvelles lignes qu'il y a de lignes, donc je termine par ; echo -n a et utilise echo -n "${line:0:-1}": ceci supprime le dernier caractère (qui est a) et conserve autant de nouvelles lignes qu'il y en avait dans l'entrée (y compris no).
57
ZyX

Essayez envsubst

FOO=foo
BAR=bar
export FOO BAR

envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF
111
yottatsa

envsubst était nouveau pour moi. Fantastique.

Pour mémoire, utiliser un heredoc est un excellent moyen de modéliser un fichier de configuration.

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
36
Dan Garthwaite

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
31
Hai Vu

Je pense que eval fonctionne vraiment bien. Il gère les modèles avec des sauts de ligne, des espaces et toutes sortes de choses bash. Si vous avez un 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"

Bien sûr, cette méthode doit être utilisée avec précaution, car eval peut exécuter du code arbitraire. Exécuter ceci en tant que root est à peu près hors de question. Les citations du modèle doivent être échappées, sinon elles seront consommées par eval.

Vous pouvez également utiliser ici des documents si vous préférez cat à echo

$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null

@plockc a fourni une solution qui évite le problème d'échappement des guillemets bash:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

Edit: Partie supprimée sur son exécution en tant que root avec Sudo ...

Edit: Ajout d'un commentaire sur la façon dont les guillemets doivent être évités, ajout de la solution de plockc au mix!

21
mogsie

J'ai une solution bash comme mogsie mais avec heredoc au lieu de herestring pour vous permettre d'éviter d'échapper aux guillemets

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
18
plockc

Modifier 6 janvier 2017

J'avais besoin de garder les guillemets doubles dans mon fichier de configuration afin de pouvoir éviter les guillemets doubles avec sed:

render_template() {
  eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}

Je ne peux pas penser à garder de nouvelles lignes, mais des lignes vides entre les deux sont conservées.


Bien qu'il s'agisse d'un sujet ancien, l'OMI a trouvé ici une solution plus élégante: 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 $1)\""
}

user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file

Tous les crédits à Grégory Pakosz .

16
CKK

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($1,0,int(length($1)/2)).($2&&length($1)%2?$2:$ENV{$3||$4});eg' template.txt

Cela étend toutes les instances de $VAR ou ${VAR} à leurs valeurs d’environnement (ou, s’ils ne sont pas définis, à la chaîne vide).

Il échappe correctement aux barres obliques inverses et accepte un $ avec barre oblique inversée pour empêcher la substitution (contrairement à envsubst, qui, il s'avère, ne le fait pas ).

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 souhaitez uniquement échapper les barres obliques inverses 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($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt
9
Stuart P. Bentley

Je l'aurais fait de cette façon, probablement moins efficace, mais plus facile à lire/à maintenir.

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
8
Craig552uk

Au lieu de réinventer la roue, allez avec envsubst Peut être utilisé dans presque tous les scénarios, par exemple la construction de fichiers de configuration à partir de variables d'environnement conteneurs docker.

Si sur mac, assurez-vous que vous avez homebrew alors reliez-le à partir de gettext:

brew install gettext
brew link --force gettext

./template.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, utilisez-le:

# 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 depends on tools that utilise .env file 
# automatically like pipenv etc. 
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh
8
smentek

Si vous souhaitez utiliser les modèles Jinja2 , voyez ce projet: j2cli .

Ça supporte:

  • Modèles à partir de fichiers JSON, INI, YAML et de flux d'entrée
  • Templating from variables d'environnement
8
kolypto

En prenant la réponse de ZyX en utilisant bash pur mais avec un nouveau style de correspondance de regex et de substitution de paramètre indirect, cela 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
5
wich

Voici une autre solution pure bash:

  • il utilise heredoc, donc:
    • la complexité n'augmente pas à cause de la syntaxe supplémentaire requise
    • le modèle peut inclure le code bash
      • cela vous permet également d'indenter des choses correctement. Voir ci-dessous.
  • il n'utilise pas eval, donc:
    • pas de problèmes avec le rendu des lignes vides à la fin
    • pas de problèmes avec les citations dans le modèle

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  echo EOT
}

cat_template template | LISTING="$LISTING" bash

$ cat template (avec des retours à la ligne et des guillemets doubles)

<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>
4
Tomáš Pospíšek

Si vous utilisez , Perl est une option et vous vous contentez de fonder les extensions sur environnement variables seulement (par opposition à toutes Shell variables), considérez le robuste de Stuart P. Bentley réponse.

Cette réponse vise à fournir une solution uniquement en bash qui, malgré l'utilisation de eval - devrait être sûr à utiliser .

Les objectifs sont les suivants:

  • Prise en charge de l'expansion des références de variable ${name} Et $name.
  • Empêcher toutes les autres extensions:
    • substitutions de commandes ($(...) et syntaxe héritée `...`)
    • substitutions arithmétiques ($((...)) et syntaxe héritée $[...]).
  • Autoriser la suppression sélective du développement de variable en préfixant avec \ (\${name}).
  • Préserver les caractères spéciaux. dans l'entrée, notamment les instances " et \.
  • Autorise les entrées via des arguments ou 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 '`([' '\1\2\3')
  # 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 '\1\2\3' '`(['
}

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 performances, la fonction lit l'entrée stdin en une fois en mémoire, mais il est facile d'adapter la fonction à une approche ligne par ligne.
  • Prend également en charge les extensions non-basic telles que ${HOME:0:10}, Dans la mesure où elles ne contiennent aucune substitution incorporée de commande ou arithmétique, telle que ${HOME:0:$(echo 10)}
    • Ces substitutions incorporées interrompront réellement la fonction (car toutes les instances $( Et ` Sont échappées à l'aveuglette).
    • De même, les références de variable mal formées telles que ${HOME (Fermeture manquante }) Interrompent la fonction.
  • En raison du traitement par bash des chaînes entre guillemets, les barres obliques inverses sont traitées comme suit:
    • \$name Empêche l'expansion.
    • Un seul \ Non suivi de $ Est conservé tel quel.
    • Si vous voulez représenter des instances multiples adjacentes\, Vous devez les doubler; par exemple.:
      • \\ -> \ - identique à \
      • \\\\ -> \\
    • L'entrée ne doit pas contenir les caractères suivants (rarement utilisés) utilisés à des fins internes: 0x1, 0x2, 0x3.
  • Il existe un problème largement hypothétique selon lequel si bash devait introduire une nouvelle syntaxe d’expansion, cette fonction ne pourrait pas empêcher de telles expansions - voir ci-dessous une solution qui n’utilise pas eval.

Si vous recherchez une solution plus restrictive que seulement prend en charge les extensions ${name} - c'est-à-dire, avec obligatoire accolades, ignorant les références $name - voir cette réponse à moi.


Voici une version améliorée de de la solution sans base de bash, eval- de la réponse acceptée:

Les améliorations sont:

  • Prise en charge de l'expansion des références de variable ${name} Et $name.
  • Prise en charge de \ - échappant aux références de variables qui ne devraient pas être développées.
  • Contrairement à la solution ci-dessus basée sur eval,
    • non-basic les extensions sont ignorées
    • les références de 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"
4
mklement0

Voici une autre solution: générer un script bash avec toutes les variables et le contenu du fichier de modèle, ce script ressemblerait à ceci:

Word=dog           
i=1                
cat << EOF         
the number is ${i} 
the Word is ${Word}

EOF                

Si nous alimentons ce script dans bash, il produira le résultat souhaité:

the number is 1
the Word is dog

Voici comment générer ce script et le nourrir dans bash:

(
    # Variables
    echo Word=dog
    echo i=1

    # add the template
    echo "cat << EOF"
    cat template.txt
    echo EOF
) | bash

Discussion

  • Les parenthèses ouvrent 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 de variables
  • Toujours dans le sous-shell, nous générons la commande cat avec HEREDOC.
  • Enfin, nous transmettons la sortie du sous-shell à bash et produisons la sortie souhaitée.
  • Si vous souhaitez rediriger cette sortie dans un fichier, remplacez la dernière ligne par:

    ) | bash > output.txt
    
3
Hai Vu

Cette page décrit un réponse avec awk

awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
3
Matt Brown

Cas parfait pour shtpl . (mon projet, il est donc peu utilisé et manque de documentation. Mais voici la solution qu’il offre de toute façon. Puissiez-vous le tester?)

Il suffit d'exécuter:

$ i=1 Word=dog sh -c "$( shtpl template.txt )"

Le résultat est:

the number is 1
the Word is dog

S'amuser.

3
zstegi
# 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 < "$1"
}

C'est la fonction pure bash ajustable à votre goût, utilisée en production et qui ne devrait casser aucune entrée. Si ça casse - faites le moi savoir.

2
ttt

Vous pouvez également utiliser bashible (qui utilise en interne la méthode d'évaluation 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

1
Jan Molič

Voici un script modifié Perl basé sur quelques-unes des autres réponses:

Perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template

Caractéristiques (basées sur mes besoins, mais devraient être faciles à modifier):

  • Ignore les développements de paramètres échappés (par exemple,\$ {VAR}).
  • Prend en charge les extensions de paramètres de la forme $ {VAR}, mais pas $ VAR.
  • Remplace $ {VAR} par une chaîne vide s'il n'y a pas d'environnement VAR.
  • Ne prend en charge que les caractères a-z, A-Z, 0-9 et le trait de soulignement (sauf les chiffres en première position).
0
Kevin

Examinez la substitution de variables simples python ici: https://github.com/jeckep/vsubst

C'est très simple à utiliser:

python subst.py --props secure.properties --src_path ./templates --dst_path ./dist
0
jeckep

Voici une fonction bash qui préserve les espaces:

# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
    while IFS='' read line; do
        eval echo \""${line}"\"
    done < "${1}"
}
0
Igor Katson