Bash Templating: cómo crear archivos de configuración a partir de plantillas con Bash?

Estoy escribiendo un script para automatizar la creación de archivos de configuración para Apache y PHP para mi propio servidor web. No quiero usar ninguna GUI como CPanel o ISPConfig.

Tengo algunas plantillas de archivos de configuración de Apache y PHP. La secuencia de comandos de Bash necesita leer plantillas, realizar sustituciones de variables y plantillas de resultados analizados en alguna carpeta. ¿Cuál es la mejor manera de hacer eso? Puedo pensar de varias maneras. ¿Cuál es el mejor o puede haber alguna forma mejor de hacerlo? Quiero hacer eso en puro Bash (es fácil en PHP, por ejemplo)

1) ¿Cómo reemplazar $ {} marcadores de posición en un archivo de texto?

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" 

Por cierto, ¿cómo redirijo la salida a un archivo externo aquí? ¿Debo escapar algo si las variables contienen, por ejemplo, comillas?

2) Usando cat & sed para reemplazar cada variable con su valor:

Dado template.txt:

 The number is ${i} The word is ${word} 

Mando:

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

Me parece malo debido a la necesidad de escapar de muchos símbolos diferentes y con muchas variables la línea será demasiado larga.

¿Puedes pensar en alguna otra solución elegante y segura?

Puedes usar esto:

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

para reemplazar todas las ${...} cadenas con las variables de entorno correspondientes (no olvides exportarlas antes de ejecutar esta secuencia de comandos).

Para pure bash esto debería funcionar (suponiendo que las variables no contengan $ {...} cadenas):

 #!/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 

. Solución que no se cuelga si RHS hace referencia a alguna variable que hace referencia a sí misma:

 #! / bin / bash
 line = "$ (cat; echo -na)"
 end_offset = $ {# line}
 while [["$ {línea: 0: $ end_offset}" = ~ (. *) (\ $ \ {([a-zA-Z _] [a-zA-Z_0-9] *) \}) (. * )]];  hacer
     PRE = "$ {BASH_REMATCH [1]}"
     POST = "$ {BASH_REMATCH [4]} $ {línea: $ end_offset: $ {# línea}}"
     VARNAME = "$ {BASH_REMATCH [3]}"
     eval 'VARVAL = "$' $ VARNAME '"'
     line = "$ PRE $ VARVAL $ POST"
     end_offset = $ {# PRE}
 hecho
 echo -n "$ {línea: 0: -1}"

ADVERTENCIA : no sé cómo manejar correctamente la entrada con NUL en bash o conservar la cantidad de nuevas líneas al final. La última variante se presenta tal como es porque las shells "aman" la entrada binaria:

  1. read interpretará barras diagonales inversas.
  2. read -r no interpretará las barras diagonales inversas, pero aún mostrará la última línea si no termina con una nueva línea.
  3. "$(…)" quitará todas las líneas nuevas que haya, así que termino con ; echo -na ; echo -na y use echo -n "${line:0:-1}" : esto descarta el último carácter (que es a ) y conserva tantas líneas nuevas que haya en la entrada (incluido el no).

Prueba envsubst

 FOO=foo BAR=bar export FOO BAR envsubst <  

envsubst era nuevo para mí. Fantástico.

Para el registro, el uso de un heredoc es una excelente manera de crear una plantilla de un archivo conf.

 STATUS_URI="/hows-it-goin"; MONITOR_IP="10.10.2.15"; cat >/etc/apache2/conf.d/mod_status.conf <  SetHandler server-status Order deny,allow Deny from all Allow from ${MONITOR_IP}  EOF 

Estoy de acuerdo con el uso de sed: es la mejor herramienta para buscar / reemplazar. Aquí está mi enfoque:

 $ 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 

Creo que eval funciona realmente bien. Maneja plantillas con saltos de línea, espacios en blanco y todo tipo de cosas bash. Si tiene un control total sobre las plantillas, por supuesto:

 $ cat template.txt variable1 = ${variable1} variable2 = $variable2 my-ip = \"$(curl -s ifconfig.me)\" $ echo $variable1 AAA $ echo $variable2 BBB $ eval "echo \"$( 

Este método se debe usar con cuidado, por supuesto, ya que eval puede ejecutar código arbitrario. Ejecutar esto como root es casi imposible. Las citas en la plantilla deben ser escapadas, de lo contrario serán evaluadas por eval .

También puede usar aquí documentos si prefiere cat to echo

 $ eval "cat < << \"$( /dev/null 

@plockc provocó una solución que evita el problema de escape cita de bash:

 $ eval "cat <  /dev/null 

Editar: parte eliminada sobre ejecutar esto como root usando sudo …

Editar: ¡Se agregó un comentario sobre cómo se deben escapar las comillas, se agregó la solución de plockc a la mezcla!

Editar 6 de enero de 2017

Necesitaba mantener comillas dobles en mi archivo de configuración, así que el doble de escapar de las comillas dobles con sed ayuda:

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

No puedo pensar en mantener nuevas líneas al final, pero las líneas vacías se mantienen.


Aunque es un tema antiguo, IMO descubrí una solución más elegante aquí: 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 

Todos los créditos a Grégory Pakosz .

Tengo una solución bash como mogsie pero con heredoc en lugar de herestring para que pueda evitar el escape de comillas dobles

 eval "cat <  /dev/null 

Lo hubiera hecho de esta manera, probablemente menos eficiente, pero más fácil de leer / mantener.

 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 

Una versión más larga pero más robusta de la respuesta aceptada:

 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 

Esto expande todas las instancias de $VAR o ${VAR} a sus valores de entorno (o, si no están definidos, la cadena vacía).

Se escapa correctamente de las barras diagonales inversas, y acepta una barra invertida: $ para evitar la sustitución (a diferencia de envsubst, que resulta que no lo hace ).

Entonces, si tu entorno es:

 FOO=bar BAZ=kenny TARGET=backslashes NOPE=engi 

y tu plantilla es:

 Two ${TARGET} walk into a \\$FOO. \\\\ \\\$FOO says, "Delete C:\\Windows\\System32, it's a virus." $BAZ replies, "\${NOPE}s." 

el resultado sería:

 Two backslashes walk into a \bar. \\ \$FOO says, "Delete C:\Windows\System32, it's a virus." kenny replies, "${NOPE}s." 

Si solo quiere escapar de las barras diagonales inversas antes de $ (puede escribir “C: \ Windows \ System32” en una plantilla sin cambios), use esta versión ligeramente modificada:

 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 

Si desea usar plantillas de Jinja2 , vea este proyecto: j2cli .

Es compatible con:

  • Plantillas de archivos JSON, INI, YAML y flujos de entrada
  • Plantilla de variables de entorno

Tomando la respuesta de ZyX usando pure bash pero con un nuevo ajuste de expresiones regulares de estilo y la sustitución indirecta de parámetros se convierte en:

 #!/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 usa Perl es una opción y se contenta con basar las expansiones en variables de entorno únicamente (a diferencia de todas las variables de shell ), considere la robusta respuesta de Stuart P. Bentley .

Esta respuesta tiene como objective proporcionar una solución exclusiva que, a pesar del uso de eval , debe ser segura de usar .

Los objectives son:

  • Admite la expansión de las referencias de variable de ${name} y $name .
  • Prevenir todas las demás expansiones:
    • sustituciones de comando ( $(...) y syntax heredada `...` )
    • sustituciones aritméticas ( $((...)) y syntax heredada $[...] ).
  • Permitir la supresión selectiva de la expansión variable mediante el prefijo \ ( \${name} ).
  • Conservar caracteres especiales. en la entrada, notablemente " y \ instancias.
  • Permitir entrada ya sea a través de argumentos o vía stdin.

Función 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' '`([' } 

Ejemplos:

 $ 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 
  • Por razones de rendimiento, la función lee la entrada estándar de una vez en la memoria, pero es fácil adaptar la función a un enfoque línea por línea.
  • También admite expansiones de variables no básicas , como ${HOME:0:10} , siempre que no contengan ningún comando incrustado o sustituciones aritméticas, como ${HOME:0:$(echo 10)}
    • Tales sustituciones incorporadas en realidad ROMPEN la función (porque todas las instancias $( y ` escapan ciegamente).
    • De forma similar, las referencias de variables mal formadas, como ${HOME (cierre faltante } ) ROMPEN la función.
  • Debido al manejo de bash de cadenas de comillas dobles, las barras diagonales inversas se manejan de la siguiente manera:
    • \$name evita la expansión.
    • Un único \ no seguido de $ se conserva como está.
    • Si desea representar varias instancias \ adyacentes , debe duplicarlas ; p.ej:
      • \\ -> \ - lo mismo que simplemente \
      • \\\\ -> \\
    • La entrada no debe contener los siguientes caracteres (poco utilizados), que se utilizan con fines internos: 0x1 , 0x2 , 0x3 .
  • Existe una preocupación en gran parte hipotética de que si bash debe introducir una nueva syntax de expansión, esta función podría no evitar tales expansiones; consulte a continuación una solución que no utiliza eval .

Si está buscando una solución más restrictiva que solo admita expansiones de ${name} , es decir, con llaves obligatorias , ignorando las referencias de $name , consulte esta respuesta mía.


Aquí hay una versión mejorada de la solución libre de eval bash-only de la respuesta aceptada :

Las mejoras son:

  • Soporte para la expansión de referencias de variables de ${name} y $name .
  • Soporte para \ -escaping referencias de variables que no deberían expandirse.
  • A diferencia de la solución basada en eval anterior,
    • expansiones no básicas son ignoradas
    • las referencias de variables malformadas se ignoran (no rompen el 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, ie, 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" 

Esta página describe una respuesta con awk

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

Estuche perfecto para shtpl . (proyecto mío, por lo que no es ampliamente utilizado y carece de documentación. Pero aquí está la solución que ofrece de todos modos. Puede que desee probarlo).

Simplemente ejecuta:

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

El resultado es:

 the number is 1 the word is dog 

Que te diviertas.

Aquí hay otra solución pura bash:

  • está usando heredoc, entonces:
    • la complejidad no aumenta debido a la syntax requerida adicionalmente
    • la plantilla puede incluir código bash
      • eso también te permite sangrar cosas apropiadamente. Vea abajo.
  • no usa eval, entonces:
    • no hay problemas con la representación de líneas vacías
    • no hay problemas con las comillas en la plantilla

$ cat code

 #!/bin/bash LISTING=$( ls ) cat_template() { echo "cat < < EOT" cat "$1" echo EOT } cat_template template | LISTING="$LISTING" bash 

$ cat template (con saltos de línea y comillas dobles)

     

"directory listing"

 $( echo "$LISTING" | sed 's/^/ /' ) 
 

salida

     

"directory listing"

 code template 
 

Aquí hay otra solución: generar un script bash con todas las variables y el contenido del archivo de plantilla, ese script se vería así:

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

Si alimentamos este script en bash, produciría el resultado deseado:

 the number is 1 the word is dog 

Aquí es cómo generar ese script y alimentar ese script en bash:

 ( # Variables echo word=dog echo i=1 # add the template echo "cat < < EOF" cat template.txt echo EOF ) | bash 

Discusión

  • Los paréntesis abren un subconjunto, su propósito es agrupar todos los resultados generados
  • Dentro del subconjunto, generamos todas las declaraciones de variables
  • También en el subconjunto, generamos el comando cat con HEREDOC
  • Finalmente, alimentamos el resultado de subcartera a bash y producimos la salida deseada
  • Si desea redirigir esta salida a un archivo, reemplace la última línea con:

     ) | bash > output.txt 

También puede usar bashible (que internamente utiliza el enfoque de evaluación descrito arriba / abajo).

Hay un ejemplo de cómo generar un HTML a partir de varias partes:

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

Esta es la función bash pura ajustable a su gusto, utilizada en producción y no debe interrumpir ninguna entrada. Si se rompe, házmelo saber.

Aquí hay una función bash que preserva el espacio en blanco:

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

Aquí hay un script modificado de perl basado en algunas de las otras respuestas:

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

Funciones (según mis necesidades, pero deberían ser fáciles de modificar):

  • Skips escapó expansiones de parámetros (por ejemplo, \ $ {VAR}).
  • Admite expansiones de parámetros del formulario $ {VAR}, pero no $ VAR.
  • Reemplaza $ {VAR} con una cadena en blanco si no hay VAR envar.
  • Solo admite az, AZ, 0-9 y caracteres de subrayado en el nombre (sin incluir los dígitos en la primera posición).

En lugar de reinventar la rueda, vaya con envsubst. Se puede usar en casi cualquier escenario, por ejemplo creando archivos de configuración a partir de variables de entorno en contenedores acoplables.

Si en mac asegúrate de tener homebrew , vincúlalo desde 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 

Ahora solo úsalo:

 # 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