resueltos - scripts bash ejemplos
Bash Templating: cómo crear archivos de configuración a partir de plantillas con Bash? (21)
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?
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 .
Aquí hay otra solución pura bash:
- está usando heredoc, entonces:
- la complejidad no aumenta debido a la sintaxis 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)
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
$( echo "$LISTING" | sed ''s/^/ /'' )
<pre>
</p>
</body>
</html>
salida
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
code
template
<pre>
</p>
</body>
</html>
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
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).
Aquí hay una función bash que preserva el espacio en blanco:
# 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}"
}
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 /"$(<template.txt)/"" 2> /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 <<< /"$(<template.txt)/"" 2> /dev/null
@plockc provocó una solución que evita el problema de escape cita de bash:
$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /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!
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
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
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.
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
Para un acercamiento a la creación de plantillas, vea mi respuesta aquí .
Prueba envsubst
FOO=foo
BAR=bar
export FOO BAR
envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF
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 -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}"
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 useecho -n "${line:0:-1}"
: esto descarta el último carácter (que esa
) y conserva tantas líneas nuevas que haya en la entrada (incluido el no).
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 objetivo proporcionar una solución exclusiva que, a pesar del uso de eval
, debe ser segura de usar .
Los objetivos son:
- Admite la expansión de las referencias de variable de
${name}
y$name
. - Prevenir todas las demás expansiones:
- sustituciones de comando (
$(...)
y sintaxis heredada`...`
) - sustituciones aritméticas (
$((...))
y sintaxis heredada$[...]
).
- sustituciones de comando (
- 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.
- Tales sustituciones incorporadas en realidad ROMPEN la función (porque todas las instancias
- 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 sintaxis 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, 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"
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
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 <<EOF
$(<template.txt)
EOF
" 2> /dev/null
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
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
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 <<EOF
<Location ${STATUS_URI}>
SetHandler server-status
Order deny,allow
Deny from all
Allow from ${MONITOR_IP}
</Location>
EOF
# 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.