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:
read
interpretará barras diagonales inversas. read -r
no interpretará las barras diagonales inversas, pero aún mostrará la última línea si no termina con una nueva línea. "$(…)"
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 \"$( /dev/null variable1 = AAA variable2 = BBB my-ip = "11.22.33.44"
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!
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:
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:
${name}
y $name
. $(...)
y syntax heredada `...`
) $((...))
y syntax heredada $[...]
). \
( \${name}
). "
y \
instancias. 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
${HOME:0:10}
, siempre que no contengan ningún comando incrustado o sustituciones aritméticas, como ${HOME:0:$(echo 10)}
$(
y `
escapan ciegamente). ${HOME
(cierre faltante }
) ROMPEN la función. \$name
evita la expansión. \
no seguido de $
se conserva como está. \
adyacentes , debe duplicarlas ; p.ej:
\\
-> \
- lo mismo que simplemente \
\\\\
-> \\
0x1
, 0x2
, 0x3
. 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:
${name}
y $name
. \
-escaping referencias de variables que no deberían expandirse. eval
anterior,
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:
$ 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
cat
con HEREDOC 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):
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