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?

89
demandé sur Community 2010-05-26 19:12:16

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:

  1. read interprétera les tirets arrière.
  2. 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.
  3. "$(…)" va rayer autant de nouvelles lignes traînantes qu'il y en a, donc je termine avec ; echo -n a et utilise echo -n "${line:0:-1}" : Cela fait tomber le dernier caractère (qui est a ) et préserve autant de nouvelles lignes traînantes qu'il y en avait dans l'entrée (y compris no).
47
répondu ZyX 2014-12-18 22:23:43

Essayer envsubst

FOO=foo
BAR=bar
export FOO BAR

envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF
91
répondu yottatsa 2012-06-15 12:48:40

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
34
répondu Dan Garthwaite 2012-08-17 13:33:14

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
29
répondu Hai Vu 2010-05-26 18:13:52

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!

16
répondu mogsie 2014-10-17 12:41:57

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 .

16
répondu CKK 2017-01-06 14:26:43

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
14
répondu plockc 2015-07-25 16:19:40

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
9
répondu Stuart P. Bentley 2015-03-25 17:57:33

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
8
répondu Craig552uk 2010-05-28 11:07:05

si vous voulez utiliser Jinja2 templates, voir ce projet: j2cli .

il supporte:

  • Modèles de JSON, INI, fichiers YAML et les flux d'entrée
  • création de modèles à partir de variables d'environnement
6
répondu kolypto 2014-06-25 15:16:58

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
5
répondu wich 2014-10-22 14:17:09

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 $[...] ).
  • 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.
  • 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"
4
répondu mklement0 2017-05-23 11:47:29

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
3
répondu Matt Brown 2012-03-06 19:39:52

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.

3
répondu zstegi 2013-03-03 17:24:14

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>
3
répondu Tomáš Pospíšek 2017-02-18 11:52:16

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
    
2
répondu Hai Vu 2018-04-04 15:00:02

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

1
répondu Jan Molič 2015-09-03 13:30:47
# 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.

1
répondu ttt 2017-10-12 18:50:21

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 < ""
}
0
répondu Igor Katson 2016-01-01 03:27:00

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).
0
répondu Kevin 2016-12-22 02:02:54

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
0
répondu smentek 2018-08-31 08:45:07