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

164

Quiero canalizar la salida de un archivo de "plantilla" en MySQL, el archivo tiene variables como ${dbName}intercaladas. ¿Cuál es la utilidad de línea de comando para reemplazar estas instancias y volcar la salida a la salida estándar?

Dana la sana
fuente

Respuestas:

192

Sed !

Dado template.txt:

El número es $ {i}
La palabra es $ {word}

solo tenemos que decir:

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

Gracias a Jonathan Leffler por el consejo de pasar múltiples -eargumentos a la misma sedinvocación.

usuario
fuente
13
Puede combinar esos dos comandos sed en uno: sed -e "s / \ $ {i} / 1 /" -e "s / \ $ {word} / dog /"; Eso es más eficiente. Puede encontrar problemas con algunas versiones de sed en quizás 100 operaciones de este tipo (problema de hace años, puede que aún no sea cierto, pero tenga cuidado con HP-UX).
Jonathan Leffler el
1
Gracias Jonathan, exactamente lo que estaba buscando.
Dana the Sane
3
Sugerencia pequeña: si "1" o "perro" en el ejemplo dado contendrían un símbolo de dólar, tendría que escapar con una barra invertida (de lo contrario, no se produce el reemplazo).
MatthieuP
9
Tampoco necesitas el cat. Todo lo que necesita es sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text.
HardlyKnowEm
3
¿Qué pasa si el texto de reemplazo es una contraseña? En este caso, sedesperará un texto escapado, lo cual es una molestia.
jpbochi
179

Actualizar

Aquí hay una solución de yottatsa en una pregunta similar que solo reemplaza variables como $ VAR o $ {VAR}, y es breve.

i=32 word=foo envsubst < template.txt

Por supuesto, si i y word están en su entorno, entonces es solo

envsubst < template.txt

En mi Mac parece que se instaló como parte de gettext y de MacGPG2

Vieja respuesta

Aquí hay una mejora a la solución de mogsie en una pregunta similar, mi solución no requiere que escale las comillas dobles, lo hace mogsie, ¡pero la suya es de una sola línea!

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

El poder de estas dos soluciones es que solo obtienes unos pocos tipos de expansiones de shell que no ocurren normalmente $ ((...)), `...` y $ (...), aunque la barra diagonal inversa es un escapar del personaje aquí, pero no tiene que preocuparse de que el análisis tenga un error, y hace varias líneas muy bien.

plockc
fuente
66
Me envsubstparece que lo desnudo no funciona si tus envars no se exportan.
Toddius Zho
44
@ToddiusZho: No existe una variable de entorno que no se exporte: es precisamente la exportación lo que hace que una variable de shell sea ​​una variable de entorno. envsubst, como su nombre lo indica, solo reconoce variables de entorno , no variables de shell . También vale la pena señalar que envsubstes una utilidad GNU y, por lo tanto, no está preinstalada ni disponible en todas las plataformas.
mklement0
2
Quizás otra forma de decir es que envsubst solo ve sus propias variables de entorno de proceso, por lo que las variables de shell "normales" que podría haber definido anteriormente (en líneas separadas) no son heredadas por procesos secundarios a menos que las "exporte". En mi ejemplo de uso de gettext anterior, estoy modificando el entorno heredado de gettext a través de un mecanismo bash prefijándolos al comando que estoy a punto de ejecutar
plockc
1
Tengo una cadena con $ HOME, encontré que $ HOME funciona como shell predeterminado, en cambio $ HOME como mi propio / home / zw963, pero parece que no admite la sustitución $ (cat / etc / hostname), por lo que no se completa con mi propia demanda.
zw963
3
Gracias por la "Respuesta anterior", ya que no solo permite variables, sino también comandos de shell como $ (ls -l)
Alek
46

Utilizar /bin/sh . Cree un pequeño script de shell que establezca las variables y luego analice la plantilla utilizando el propio shell. Me gusta así (edite para manejar líneas nuevas correctamente):

Archivo template.txt:

the number is ${i}
the word is ${word}

Archivo 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"

Salida:

#sh script.sh
the number is 1
the word is dog
GNUD
fuente
2
Por qué no solo: mientras lee la línea; ¿eval echo "$ line"; hecho <./template.txt ??? No es necesario leer todo el archivo en la memoria, solo escupirlo una línea a la vez mediante el uso intensivo de la cabeza y la cola. Pero 'eval' está bien, a menos que la plantilla contenga caracteres de shell como comillas inversas.
Jonathan Leffler el
16
Esto es muy peligroso! bashSe ejecutará todo el comando en la entrada. Si la plantilla es: "las palabras son; rm -rf $ HOME", perderá archivos.
rzymek
1
@rzymek: recuerde que quiere canalizar este archivo directamente a la base de datos. Aparentemente, la entrada es confiable.
gnud
44
@gnud Hay una diferencia entre confiar en un archivo lo suficiente como para almacenar su contenido y confiar lo suficiente como para ejecutar todo lo que contiene.
Mark
3
Para tener en cuenta las restricciones: (a) las comillas dobles en la entrada se descartan silenciosamente, (b) el readcomando, como está escrito, recorta los espacios en blanco iniciales y finales de cada línea y \ caracteres 'come' ., (C) solo use esto si confiar o controlar la entrada, porque las sustituciones de comandos ( `…` o $(…)) incrustadas en la entrada permiten la ejecución de comandos arbitrarios debido al uso de eval. Finalmente, existe una pequeña posibilidad de que echoconfunda el comienzo de una línea con una de sus opciones de línea de comandos.
mklement0
23

Estaba pensando en esto nuevamente, dado el interés reciente, y creo que la herramienta en la que estaba pensando originalmente era m4el macroprocesador para autotools. Entonces, en lugar de la variable que especifiqué originalmente, usaría:

$echo 'I am a DBNAME' | m4 -DDBNAME="database name"
Dana la sana
fuente
1
Esta solución tiene la menor cantidad de inconvenientes de las respuestas aquí. Sin embargo, ¿conoce alguna forma de reemplazar $ {DBNAME} en lugar de solo DBNAME?
Jack Davidson el
@JackDavidson Lo usaría envsubstpara este uso simple de reemplazo / plantilla variable, como se menciona en otras respuestas. m4es una gran herramienta, pero es un preprocesador completo con muchas más funciones y, por lo tanto, complejidad que puede no ser necesaria si simplemente desea reemplazar algunas variables.
Imiric
13

template.txt

Variable 1 value: ${var1}
Variable 2 value: ${var2}

data.sh

#!/usr/bin/env bash
declare var1="value 1"
declare var2="value 2"

parser.sh

#!/usr/bin/env bash

# args
declare file_data=$1
declare file_input=$2
declare file_output=$3

source $file_data
eval "echo \"$(< $file_input)\"" > $file_output

./parser.sh data.sh template.txt parsed_file.txt

parsed_file.txt

Variable 1 value: value 1
Variable 2 value: value 2
ChaPuZ
fuente
1
Como se ha señalado en otra parte: solo use esto si confía o controla completamente la entrada, porque las sustituciones de comandos ( `…` o $(…)) incrustadas en la entrada permiten la ejecución de comandos arbitrarios debido al uso de eval, y la ejecución directa del código de shell debido al uso de source. Además, las comillas dobles en la entrada se descartan silenciosamente y echopodrían confundir el comienzo de una línea con una de sus opciones de línea de comandos.
mklement0
Desafortunadamente, esto elimina todas las comillas dobles (") del archivo de resultados. ¿Hay alguna manera de hacer lo mismo sin eliminar las comillas dobles?"
Ivaylo Slavov
Encontré lo que estaba buscando aquí: stackoverflow.com/a/11050943/795158 ; Solía ​​envsubst. La diferencia es que los vars deben exportarse, lo cual estuvo bien para mí.
Ivaylo Slavov
si el archivo de texto contiene "` "o". " , el sustituto fallará.
shuiqiang
12

Aquí hay una función Bash robusta que, a pesar de usar eval, debería ser segura de usar.

Todas ${varName}las referencias de variables en el texto de entrada se expanden en función de las variables del shell de llamada.

No se expande nada más : ni referencias de variables cuyos nombres no están encerrados {...}(como $varName), ni sustituciones de comandos ( $(...)y sintaxis heredada `...`), ni sustituciones aritméticas ( $((...))y sintaxis heredada)$[...] ).

Para tratar a $como un literal, \-escápelo; p.ej:\${HOME}

Tenga en cuenta que la entrada solo se acepta a través de stdin .

Ejemplo:

$ expandVarsStrict <<<'$HOME is "${HOME}"; `date` and \$(ls)' # only ${HOME} is expanded
$HOME is "/Users/jdoe"; `date` and $(ls)

Código fuente de la función:

expandVarsStrict(){
  local line lineEscaped
  while IFS= read -r line || [[ -n $line ]]; do  # the `||` clause ensures that the last line is read even if it doesn't end with \n
    # Escape ALL chars. that could trigger an expansion..
    IFS= read -r -d '' lineEscaped < <(printf %s "$line" | tr '`([$' '\1\2\3\4')
    # ... then selectively reenable ${ references
    lineEscaped=${lineEscaped//$'\4'{/\${}
    # Finally, escape embedded double quotes to preserve them.
    lineEscaped=${lineEscaped//\"/\\\"}
    eval "printf '%s\n' \"$lineEscaped\"" | tr '\1\2\3\4' '`([$'
  done
}

La función asume que no 0x1, 0x2, 0x3, y 0x4caracteres de control están presentes en la entrada, porque esos caracteres. se usan internamente, ya que la función procesa texto , debería ser una suposición segura.

mklement0
fuente
2
Esta es una de las mejores respuestas aquí. Incluso con el uso evales bastante seguro de usar.
anubhava
1
¡Esta solución funciona con archivos JSON! (¡escapando "correctamente!)
WBAR
2
Lo bueno de esta solución es que le permitirá proporcionar valores predeterminados para las variables que faltan ${FOO:-bar}o solo generará algo si está configurado ${HOME+Home is ${HOME}}. Sospecho que con una pequeña extensión también podría devolver códigos de salida para las variables que faltan, ${FOO?Foo is missing}pero actualmente tldp.org/LDP/abs/html/parameter-substitution.html tiene una lista de estos si eso ayuda
Stuart Moore
11

Crear rendertemplate.sh:

#!/usr/bin/env bash

eval "echo \"$(cat $1)\""

Y template.tmpl:

Hello, ${WORLD}
Goodbye, ${CHEESE}

Renderice la plantilla:

$ export WORLD=Foo
$ CHEESE=Bar ./rendertemplate.sh template.tmpl 
Hello, Foo
Goodbye, Bar
neu242
fuente
2
Esto
elimina las
Intenté: eval "echo $ (cat $ 1)" - sin cotizaciones, y funcionó para mí.
access_granted
2
Desde una perspectiva de seguridad, estas son malas noticias. Si su plantilla contiene $(rm -rf ~), lo está ejecutando como código.
Charles Duffy
eval "echo \"$(cat $1)\"" Funciona genial !
dev devv
10

Aquí está mi solución con perl basada en la respuesta anterior, reemplaza las variables de entorno:

perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1}?$ENV{$1}:"missing variable $1")/eg' < infile > outfile
Thomas
fuente
2
Esto es genial. No siempre tienes perl, pero cuando lo haces, esto es simple y directo.
Aaron McMillin
5

Si estás abierto a usar Perl , esa sería mi sugerencia. Aunque probablemente haya algunos expertos en sed y / o AWK que probablemente sepan cómo hacerlo mucho más fácilmente. Si tiene un mapeo más complejo con algo más que dbName para sus reemplazos, podría extender esto con bastante facilidad, pero también podría incluirlo en un script Perl estándar en ese momento.

perl -p -e 's/\$\{dbName\}/testdb/s' yourfile | mysql

Un breve script de Perl para hacer algo un poco más complicado (manejar múltiples teclas):

#!/usr/bin/env perl
my %replace = ( 'dbName' => 'testdb', 'somethingElse' => 'fooBar' );
undef $/;
my $buf = <STDIN>;
$buf =~ s/\$\{$_\}/$replace{$_}/g for keys %replace;
print $buf;

Si nombra el guión anterior como guión de reemplazo, podría usarse de la siguiente manera:

replace-script < yourfile | mysql
Beau Simensen
fuente
1
Funciona para variables individuales, pero ¿cómo incluyo 'o' para las demás?
Dana the Sane
2
Hay muchas maneras de hacer esto con Perl, todo dependiendo de cuán complicado y / o seguro quiera hacer esto. Se pueden encontrar ejemplos más complicados aquí: perlmonks.org/?node_id=718936
Beau Simensen el
3
Usar perl es mucho más limpio que intentar usar el shell. Dedique tiempo a hacer que esto funcione en lugar de probar algunas de las otras soluciones basadas en shell mencionadas.
jdigital
1
Recientemente tuve que abordar un problema similar. Al final fui con Perl (envsubst parecía prometedor por un momento, pero era demasiado difícil de controlar).
sfitts
5

Aquí hay una manera de hacer que el shell realice la sustitución por usted, como si el contenido del archivo estuviera escrito entre comillas dobles.

Usando el ejemplo de template.txt con contenido:

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

La siguiente línea hará que el shell interpole el contenido de template.txt y escriba el resultado en estándar.

i='1' word='dog' sh -c 'echo "'"$(cat template.txt)"'"'

Explicación:

  • iy wordse pasan como variables de entorno limitadas a la ejecución de sh.
  • sh ejecuta el contenido de la cadena que se pasa.
  • Las cadenas escritas una al lado de la otra se convierten en una cadena, esa cadena es:
    • ' echo "' + " $(cat template.txt)" + ' "'
  • Como la sustitución está en el medio ", " $(cat template.txt)" se convierte en la salida decat template.txt .
  • Entonces el comando ejecutado por se sh -cconvierte en:
    • echo "The number is ${i}\nThe word is ${word}",
    • donde iy wordson las variables de entorno especificadas.
A priori
fuente
Desde una perspectiva de seguridad, estas son malas noticias. Si su plantilla contiene, digamos, '$(rm -rf ~)'$(rm -rf ~)las comillas literales en el archivo de plantilla coincidirán con las que agregó antes de su expansión.
Charles Duffy
No creo que las comillas dentro de la plantilla coincidan con las comillas fuera de la plantilla, creo que el shell está resolviendo la plantilla y la cadena en la terminal de forma independiente (eliminando las comillas) y luego concatenándolas. Una versión de la prueba que no elimina su directorio de inicio es '$(echo a)'$(echo a). Produce 'a'a. Lo principal que está sucediendo es que lo primero echo adentro de esto 'es ser evaluado, lo que puede no ser lo que esperas ya que está dentro ', pero es el mismo comportamiento que incluir 'en una "cadena entre comillas.
Abriori
Por lo tanto, esto no es seguro en el sentido de que permite que el autor de la plantilla ejecute su código. Sin embargo, cómo se evalúan las citas realmente no afecta la seguridad. Expandir cualquier cosa con una "cadena entre comillas (incluido $(...)) es el punto.
Abriori
¿Es ese el punto? Solo los veo pidiendo ${varname}, no otras expansiones de mayor riesgo de seguridad.
Charles Duffy
... Dicho esto, debo diferir (re: las plantillas dentro y fuera de la plantilla pueden coincidir). Cuando pone una comilla simple en su cadena, se divide en una cadena de comillas simples echo ", seguida de una cadena de comillas dobles con los contenidos literales de template.txt, seguido de otra cadena literal ", todos concatenados en un solo argumento pasado a sh -c. Tiene razón en que 'no se puede hacer coincidir (ya que fue consumido por la capa externa en lugar de pasar a la interna), pero "ciertamente puede, por lo que se Gotcha"; rm -rf ~; echo "podría ejecutar una plantilla que contenga .
Charles Duffy
4

file.tpl:

The following bash function should only replace ${var1} syntax and ignore 
other shell special chars such as `backticks` or $var2 or "double quotes". 
If I have missed anything - let me know.

script.sh:

template(){
    # usage: template file.tpl
    while read -r line ; do
            line=${line//\"/\\\"}
            line=${line//\`/\\\`}
            line=${line//\$/\\\$}
            line=${line//\\\${/\${}
            eval "echo \"$line\""; 
    done < ${1}
}

var1="*replaced*"
var2="*not replaced*"

template file.tpl > result.txt
usuario976433
fuente
2
Esto no es seguro ya que ejecutará sustituciones de comandos en la plantilla si tienen una barra invertida principal, por ejemplo\$(date)
Peter Dolberg,
1
Aparte del punto válido de Peter: le sugiero que lo use while IFS= read -r line; docomo readcomando, de lo contrario, eliminará los espacios en blanco iniciales y finales de cada línea de entrada. Además, echopodría confundir el comienzo de una línea con una de sus opciones de línea de comandos, por lo que es mejor usarla printf '%s\n'. Finalmente, es más seguro hacer comillas dobles ${1}.
mklement0
4

Sugeriría usar algo como Sigil : https://github.com/gliderlabs/sigil

Se compila en un solo binario, por lo que es extremadamente fácil de instalar en sistemas.

Luego puede hacer una línea simple como la siguiente:

cat my-file.conf.template | sigil -p $(env) > my-file.conf

Esto es mucho más seguro evaly más fácil que usar regex osed

spudfkc
fuente
2
¡Gran respuesta! Es un sistema de plantillas adecuado y mucho más fácil de trabajar que las otras respuestas.
Erfan
Por cierto, es mejor evitar caty usar <my-file.conf.templateen su lugar para que le dé sigilun identificador de archivo real en lugar de un FIFO.
Charles Duffy
2

Encontré este hilo mientras me preguntaba lo mismo. Me inspiró a esto (cuidado con los backticks)

$ echo $MYTEST
pass!
$ cat FILE
hello $MYTEST world
$ eval echo `cat FILE`
hello pass! world
GlueC
fuente
44
Una taquigrafía para $(cat file)es$(< file)
Glenn Jackman
3
Aparentemente, este método se equivoca con los saltos de línea, es decir, mi archivo se repitió en una sola línea.
Arthur Corenzan
@ArthurCorenzan: De hecho, los saltos de línea se reemplazan con espacios. Para solucionarlo, tendrías que usarlo, eval echo "\"$(cat FILE)\""pero aún puede fallar en que las comillas dobles en la entrada se descartan.
mklement0
Como se ha señalado en otra parte: solo use esto si confía o controla completamente la entrada, porque las sustituciones de comandos ( `…` o $(…)) incrustadas en la entrada permiten la ejecución de comandos arbitrarios debido al uso de eval.
mklement0
2

Muchas opciones aquí, pero pensé que tiraría la mía al montón. Está basado en Perl, solo se dirige a variables de la forma $ {...}, toma el archivo para procesarlo como argumento y genera el archivo convertido en stdout:

use Env;
Env::import();

while(<>) { $_ =~ s/(\${\w+})/$1/eeg; $text .= $_; }

print "$text";

Por supuesto, no soy realmente una persona perl, por lo que fácilmente podría haber un defecto fatal (aunque funciona para mí).

sfitts
fuente
1
Funciona bien. Podría soltar la Env::import();línea; la importación está implícita en use. Además, sugiero no construir primero toda la salida en la memoria: simplemente use en print;lugar de $text .= $_;dentro del bucle y suelte el printcomando post-loop .
mklement0
1

Se puede hacer en bash si tienes control del formato del archivo de configuración. Solo necesita fuente (".") Del archivo de configuración en lugar de subshell. Eso garantiza que las variables se creen en el contexto del shell actual (y continúen existiendo) en lugar de la subshell (donde la variable desaparece cuando la subshell sale).

$ cat config.data
    export parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA
    export parm_user=pax
    export parm_pwd=never_you_mind

$ cat go.bash
    . config.data
    echo "JDBC string is " $parm_jdbc
    echo "Username is    " $parm_user
    echo "Password is    " $parm_pwd

$ bash go.bash
    JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
    Username is     pax
    Password is     never_you_mind

Si su archivo de configuración no puede ser un script de shell, simplemente puede 'compilarlo' antes de ejecutarlo (la compilación depende de su formato de entrada).

$ cat config.data
    parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA # JDBC URL
    parm_user=pax                              # user name
    parm_pwd=never_you_mind                    # password

$ cat go.bash
    cat config.data
        | sed 's/#.*$//'
        | sed 's/[ \t]*$//'
        | sed 's/^[ \t]*//'
        | grep -v '^$'
        | sed 's/^/export '
        >config.data-compiled
    . config.data-compiled
    echo "JDBC string is " $parm_jdbc
    echo "Username is    " $parm_user
    echo "Password is    " $parm_pwd

$ bash go.bash
    JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
    Username is     pax
    Password is     never_you_mind

En su caso específico, podría usar algo como:

$ cat config.data
    export p_p1=val1
    export p_p2=val2
$ cat go.bash
    . ./config.data
    echo "select * from dbtable where p1 = '$p_p1' and p2 like '$p_p2%' order by p1"
$ bash go.bash
    select * from dbtable where p1 = 'val1' and p2 like 'val2%' order by p1

Luego canalice la salida de go.bash en MySQL y listo, con suerte no destruirá su base de datos :-).

paxdiablo
fuente
1
No tiene que exportar las variables desde el archivo config.data; es suficiente solo configurarlos. Tampoco parece estar leyendo el archivo de plantilla en ningún momento. O, tal vez, el archivo de plantilla se modifica y contiene las operaciones 'echo' ... ¿o me falta algo?
Jonathan Leffler el
1
Buen punto sobre las exportaciones, lo hago de manera predeterminada para que estén disponibles para las subcapas y no cause ningún daño ya que mueren cuando salen. El archivo 'plantilla' es el script mismo con sus declaraciones echo. No es necesario introducir un tercer archivo: es básicamente una operación de tipo mailmerge.
paxdiablo
1
El "script en sí con sus declaraciones de eco" no es una plantilla: es un script. Piense en la diferencia de legibilidad (y mantenibilidad) entre <xml type = "$ TYPE"> y echo '<xml type = "' $ TYPE '">'
Pierre-Olivier Vares
1
@Pierre, no hay declaraciones de eco en mi script de configuración, solo son exportaciones, y he mostrado cómo puedes evitar incluso eso con una cantidad mínima de preprocesamiento. Si está hablando de la declaración de eco en mis otros scripts (como go.bash), tiene el extremo equivocado del palo: no son parte de la solución, son solo una forma de mostrar que las variables están siendo ajustado correctamente
paxdiablo
1
@paxdiablo: Parece que olvidaste la pregunta: << Quiero canalizar la salida de un archivo de "plantilla" en MySQL >>. Entonces, el uso de una plantilla ES la pregunta, no es "el extremo equivocado del palo". Exportar variables y hacerlas eco en otro script simplemente no responde la pregunta en absoluto
Pierre-Olivier Vares
0

Edición perl de archivos potencialmente múltiples, con copias de seguridad.

  perl -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : ""/eg' \
    -i.orig \
    -p config/test/*
joehep
fuente
0

Querrá algo más robusto que las sugerencias actuales porque, aunque funcionan para su caso de uso limitado (por ahora), no serán suficientes para situaciones más complejas.

Necesitas un mejor renderizador. Necesitas el mejor renderizador. ¡Necesitas The Renderest!

Dado template.txt:

Hola persona}}!

Correr:

$ persona = Bob ./render template.txt

Y verás la salida

¡Hola Bob!

Escríbalo a un archivo redirigiendo stdout a un archivo:

$ persona = Bob ./render template.txt >dered.txt

Y si está renderizando un script que tiene $ {} variables que no desea interpolar, ¡The Renderest lo tiene cubierto sin tener que hacer nada más!

Continúe y obtenga su copia en https://github.com/relaxdiego/renderest

Mark Maglana
fuente