Como substituir espaços reservados $ {} em um arquivo de texto?


163

Eu quero canalizar a saída de um arquivo "template" para o MySQL, o arquivo tendo variáveis ​​como ${dbName}intercaladas. Qual é o utilitário de linha de comando para substituir essas instâncias e despejar a saída na saída padrão?

Respostas:


192

Sed !

Dado template.txt:

O número é $ {i}
A palavra é $ {word}

nós apenas temos que dizer:

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

Agradecemos a Jonathan Leffler pela dica de passar vários -eargumentos para a mesma sedinvocação.


13
Você pode combinar esses dois comandos sed em um: sed -e "s / \ $ {i} / 1 /" -e "s / \ $ {word} / dog /"; isso é mais eficiente. Você pode ter problemas com algumas versões do sed em talvez 100 operações desse tipo (problema de anos atrás - ainda não é verdade, mas tenha cuidado com o HP-UX).
Jonathan Leffler

1
Obrigado Jonathan, exatamente o que eu estava procurando.
Dana the Sane

3
Dica pequena: se "1" ou "cachorro" no exemplo dado contiver um símbolo de dólar, você deverá escapar com uma barra invertida (caso contrário, a substituição não ocorrerá).
21415 MatthieuP

9
Você também não precisa do cat. Tudo que você precisa é sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text.
precisa saber é o seguinte

3
E se o texto de substituição for uma senha? Nesse caso, sedespera-se um texto escapado, que é um aborrecimento.
jpbochi

177

Atualizar

Aqui está uma solução da yottatsa em uma pergunta semelhante que substitui apenas variáveis ​​como $ VAR ou $ {VAR} e é uma breve abordagem

i=32 word=foo envsubst < template.txt

É claro que se eu e o word estamos no seu ambiente, é apenas

envsubst < template.txt

No meu Mac, parece que ele foi instalado como parte do gettext e do MacGPG2

Resposta antiga

Aqui está uma melhoria para a solução do mogsie em uma pergunta semelhante: minha solução não exige que você escale aspas duplas, o mogsie faz, mas o dele é único!

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

O poder dessas duas soluções é que você obtém apenas alguns tipos de expansões de shell que não ocorrem normalmente $ ((...)), `...` e $ (...), embora a barra invertida seja uma aqui, mas você não precisa se preocupar com o fato de a análise ter um bug e executar várias linhas.


6
Estou descobrindo que o nada envsubstnão funciona se seus envares não forem exportados.
Toddius Zho

4
@ToddiusZho: Não existe uma variável de ambiente que não seja exportada - é exatamente a exportação que faz de uma variável de shell uma variável de ambiente. envsubst, como o nome sugere, reconhece apenas variáveis ​​de ambiente , não variáveis ​​de shell . Também vale a pena notar que envsubsté um utilitário GNU e, portanto, não está pré-instalado ou disponível em todas as plataformas.
mklement0

2
Talvez outra maneira de dizer é que o envsubst apenas vê suas próprias variáveis ​​de ambiente de processo, portanto, variáveis ​​de shell "normais" que você pode ter definido anteriormente (em linhas separadas) não são herdadas por processos filhos, a menos que você as "exporte". No meu exemplo o uso de gettext acima, eu estou modificando o ambiente gettext herdado através de um mecanismo festa prefixando-os para o comando que estou prestes a executar
plockc

1
Eu tenho uma string com $ HOME, achei que $ HOME é trabalhado como shell padrão para fazer, em vez de $ HOME como meu / home / zw963, mas parece que não suporta a substituição de $ (cat / etc / hostname), para que não seja totalmente compatível com minha própria demanda.
zw963

3
Obrigado pela "velha resposta", pois não só permite que as variáveis, mas também uma comandos shell como $ (ls -l)
Alek

46

Use /bin/sh. Crie um pequeno script de shell que defina as variáveis ​​e analise o modelo usando o próprio shell. Assim (edite para lidar com as novas linhas corretamente):

Arquivo template.txt:

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

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

Resultado:

#sh script.sh
the number is 1
the word is dog

2
Por que não apenas: enquanto lê a linha; eval echo "$ line"; done <./template.txt ??? Não há necessidade de ler o arquivo inteiro na memória, apenas para cuspi-lo uma linha de cada vez através do uso intensivo de cabeça e cauda. Mas o 'eval' está OK - a menos que o modelo contenha caracteres de shell como aspas.
Jonathan Leffler

16
Isso é muito perigoso! Todo o bashcomando na entrada será executado. Se o modelo for: "as palavras são; rm -rf $ HOME", você perderá os arquivos.
precisa saber é o seguinte

1
@rzymek - lembre-se, ele quer canalizar esse arquivo diretamente para o banco de dados. Portanto, aparentemente, a entrada é confiável.
Gnud 26/04

4
@gnud Há uma diferença entre confiar em um arquivo o suficiente para armazenar seu conteúdo e confiar o suficiente para executar qualquer coisa que ele contenha.
Mark

3
Para observar as restrições: (a) aspas duplas na entrada são silenciosamente descartadas, (b) o readcomando, conforme escrito, apara os espaços em branco à esquerda e à direita de cada linha e 'come' \ caracteres., (C) use somente se você confie ou controle a entrada, porque as substituições de comandos ( `…` ou $(…)) incorporadas na entrada permitem a execução de comandos arbitrários devido ao uso de eval. Finalmente, há uma pequena chance de echoconfundir o início de uma linha com uma de suas opções de linha de comando.
mklement0

23

Eu estava pensando sobre isso de novo, dado o interesse recente, e acho que a ferramenta em que estava pensando originalmente era m4o processador de macro para ferramentas automáticas. Então, em vez da variável especificada originalmente, você usaria:

$echo 'I am a DBNAME' | m4 -DDBNAME="database name"

1
Esta solução tem o menor número de desvantagens das respostas aqui. Você conhece alguma maneira de substituir $ {DBNAME} em vez de apenas DBNAME?
Jack Davidson

@JackDavidson Eu usaria envsubstpara esse uso simples de substituição / modelagem de variáveis, conforme mencionado em outras respostas. m4é uma ótima ferramenta, mas é um pré-processador completo com muito mais recursos e, portanto, complexidade que pode não ser necessária se você simplesmente quiser substituir algumas variáveis.
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

1
Como foi observado em outro lugar: Use isso somente se você confiar ou controlar totalmente a entrada, porque substituições de comando ( `…` ou$(…) ) incorporadas na entrada permitem a execução de comandos arbitrários devido ao uso de evale a execução direta do código do shell devido ao uso de source. Além disso, as aspas duplas na entrada são descartadas silenciosamente e echopodem confundir o início de uma linha com uma de suas opções de linha de comando.
mklement0

Infelizmente, isso retira todas as aspas ( ") a partir do arquivo de resultado Existe uma maneira de fazer o mesmo sem remover as aspas.?
Ivaylo Slavov

Encontrei o que estava procurando aqui: stackoverflow.com/a/11050943/795158 ; Eu usei o envsubst. A diferença é que os vars precisam ser exportados, o que foi bom para mim.
Ivaylo Slavov

se o arquivo de texto contiver "` "ou". " , substitude falhou.
shuiqiang 16/07

12

Aqui está uma função Bash robusta que, apesar de ser usada, evaldeve ser segura.

Todas ${varName}as referências de variáveis ​​no texto de entrada são expandidas com base nas variáveis ​​do shell de chamada.

Nada mais é expandido: nem referências de variáveis ​​cujos nomes não estão entre {...}(como $varName), nem substituições de comandos ( $(...)e sintaxe herdada `...`), nem substituições aritméticas ( $((...))e sintaxe herdada $[...]).

Para tratar a $como um literal, \escape-o; por exemplo:\${HOME}

Observe que a entrada é aceita apenas via stdin .

Exemplo:

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

Código fonte da função:

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
}

A função assume que nenhuma 0x1, 0x2, 0x3, e0x4 caracteres de controlo estão presentes na entrada, porque esses caracteres. são usados ​​internamente - como a função processa o texto , isso deve ser uma suposição segura.


2
Esta é uma das melhores respostas aqui. Mesmo com o uso eval, é bastante seguro.
Anubhava

1
Esta solução funciona com arquivos JSON! (escapar "corretamente!)
WBAR

2
Uma coisa boa com esta solução é permitir que você forneça padrões para variáveis ​​ausentes ${FOO:-bar}ou apenas produza algo se estiver definido - ${HOME+Home is ${HOME}}. Eu suspeito que com um pouco de extensão também pode retornar códigos de saída para variáveis faltando ${FOO?Foo is missing}, mas atualmente não tldp.org/LDP/abs/html/parameter-substitution.html tem uma lista de estes se isso ajuda
Stuart Moore

11

Criar rendertemplate.sh:

#!/usr/bin/env bash

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

E template.tmpl:

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

Renderize o modelo:

$ export WORLD=Foo
$ CHEESE=Bar ./rendertemplate.sh template.tmpl 
Hello, Foo
Goodbye, Bar

2
Este retira cadeias entre aspas duplas
vrtx54234

Tentei: eval "echo $ (cat $ 1)" - sem aspas, e funcionou para mim.
ACCESS_GRANTED

2
Do ponto de vista da segurança, isso são más notícias. Se o seu modelo contiver $(rm -rf ~), você o executará como código.
Charles Duffy

eval "echo \"$(cat $1)\"" Funciona bem !
dev devv 5/07

10

aqui está minha solução com perl com base na resposta anterior, substitui as variáveis ​​de ambiente:

perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1}?$ENV{$1}:"missing variable $1")/eg' < infile > outfile

2
Isso é ótimo. Nem sempre tem perl, mas quando você faz isso é simples e direto.
Aaron McMillin

5

Se você está aberto a usar Perl , essa seria minha sugestão. Embora haja provavelmente alguns especialistas em sed e / ou AWK que provavelmente sabem como fazer isso com muito mais facilidade. Se você tiver um mapeamento mais complexo com mais do que apenas dbName para suas substituições, poderá estendê-lo com bastante facilidade, mas é melhor colocá-lo em um script Perl padrão nesse momento.

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

Um script Perl curto para fazer algo um pouco mais complicado (manipular várias chaves):

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

Se você nomear o script acima como substituir-script, ele poderá ser usado da seguinte maneira:

replace-script < yourfile | mysql

1
Funciona para variáveis ​​únicas, mas como faço para incluir 'ou' para as outras?
Dana the Sane

2
Existem várias maneiras de fazer isso com o perl, tudo dependendo de quão complicado e / ou seguro você deseja fazer isso. Mais exemplos complicados pode ser encontrada aqui: perlmonks.org/?node_id=718936
Beau Simensen

3
Usar perl é muito mais limpo do que tentar usar o shell. Gaste tempo para fazer isso funcionar, em vez de tentar algumas das outras soluções baseadas em shell mencionadas.
jdigital

1
Recentemente, tivemos que resolver um problema semelhante. No final, eu fui com perl (o envsubst pareceu promissor por um tempo, mas era muito difícil de controlar).
sfitts

5

Aqui está uma maneira de fazer com que o shell substitua você, como se o conteúdo do arquivo fosse digitado entre aspas duplas.

Usando o exemplo de template.txt com o conteúdo:

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

A linha a seguir fará com que o shell interpole o conteúdo do template.txt e grave o resultado no padrão.

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

Explicação:

  • ie wordsão passados ​​como variáveis ​​de ambiente incluídas na execução de sh.
  • sh executa o conteúdo da cadeia de caracteres pela qual é passada.
  • Seqüências de caracteres escritas próximas umas das outras se tornam uma sequência, essa sequência é:
    • ' echo "' + " $(cat template.txt)" + ' "'
  • Como a substituição é intermediária ", " $(cat template.txt)" se torna a saída de cat template.txt.
  • Portanto, o comando executado por sh -cse torna:
    • echo "The number is ${i}\nThe word is ${word}",
    • onde ie wordsão as variáveis ​​de ambiente especificadas.

Do ponto de vista da segurança, isso são más notícias. Se o seu modelo contiver, digamos, '$(rm -rf ~)'$(rm -rf ~)as aspas literais no arquivo do modelo corresponderão às que você adicionou antes da expansão.
Charles Duffy

As aspas dentro do modelo não coincidem com as aspas fora do modelo, acredito que o shell esteja resolvendo o modelo e a string no terminal independentemente (removendo efetivamente as aspas) e concatenando-as. É uma versão do teste que não exclui o diretório inicial '$(echo a)'$(echo a). Produz 'a'a. A principal coisa que está acontecendo é que a primeira echo adentro da 'está sendo avaliada, o que pode não ser o que você espera desde que está presente ', mas é o mesmo comportamento de incluir 'em uma "sequência de caracteres citada.
Apriori

Portanto, isso não é seguro no sentido em que permite que o autor do modelo tenha seu código executado. No entanto, como as cotações são avaliadas realmente não afeta a segurança. Expandir qualquer coisa que seja uma "string entre aspas (incluindo $(...)) é o ponto.
Apriori

Esse é o ponto? Só os vejo pedindo ${varname}, e não outras, expansões de alto risco à segurança.
Charles Duffy

... dito isso, devo diferir (as cotas re: in-template e out-template podem ser compatíveis). Quando você coloca uma aspas simples em sua string, você está se dividindo em uma string de aspas simples echo ", seguida por uma string de aspas duplas com os contornos literais de template.txt, seguida por outra string literal ", todas concatenadas em um único argumento passado para sh -c. Você está certo que 'não pode ser correspondido (já que foi consumido pelo shell externo em vez de passado para o interno), mas "certamente pode, portanto, um modelo contendo Gotcha"; rm -rf ~; echo "pode ser executado.
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

2
Esta não é seguro, uma vez que irá executar substituições de comando no modelo se eles têm um líder barra invertida por exemplo\$(date)
Peter Dolberg

1
Além do ponto válido de Peter: sugiro que você use while IFS= read -r line; docomo readcomando, caso contrário, você removerá os espaços em branco à esquerda e à direita de cada linha de entrada. Além disso, echopode confundir o início de uma linha com uma de suas opções de linha de comando, por isso é melhor usá-lo printf '%s\n'. Finalmente, é mais seguro fazer aspas duplas ${1}.
usar o seguinte comando

4

Sugiro usar algo como Sigil : https://github.com/gliderlabs/sigil

Ele é compilado em um único binário, por isso é extremamente fácil de instalar nos sistemas.

Em seguida, você pode fazer uma linha simples como a seguinte:

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

Isso é muito mais seguro evale fácil do que usar regex oused


2
Ótima resposta! É um sistema de modelos adequado e muito mais fácil de trabalhar do que as outras respostas.
Erfan

BTW, é melhor evitar cate usar <my-file.conf.templatepara fornecer sigilum identificador de arquivo real em vez de um FIFO.
Charles Duffy

2

Eu encontrei este tópico enquanto me perguntava a mesma coisa. Isso me inspirou a isso (cuidado com os bastidores)

$ echo $MYTEST
pass!
$ cat FILE
hello $MYTEST world
$ eval echo `cat FILE`
hello pass! world

4
Um atalho do bash para $(cat file)is$(< file)
glenn jackman 16/05

3
Aparentemente, esse método atrapalha as quebras de linha, ou seja, meu arquivo é repetido em uma linha.
Arthur Corenzan

@ArthurCorenzan: De fato, as quebras de linha são substituídas por espaços. Para corrigir isso, você precisaria usar, eval echo "\"$(cat FILE)\""mas isso ainda pode não ser suficiente, pois as aspas duplas na entrada são descartadas.
mklement0

Como foi observado em outro lugar: Use isso somente se você confiar ou controlar totalmente a entrada, porque as substituições de comandos ( `…` ou $(…)) incorporadas na entrada permitem a execução de comandos arbitrários devido ao uso de eval.
mklement0

2

Muitas opções aqui, mas achei que eu jogaria o meu na pilha. Ele é baseado em perl, segmenta apenas variáveis ​​do formato $ {...}, pega o arquivo para processar como argumento e gera o arquivo convertido no stdout:

use Env;
Env::import();

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

print "$text";

Claro que eu não sou realmente uma pessoa perl, então poderia facilmente haver uma falha fatal (funciona para mim).


1
Funciona bem. Você pode largar a Env::import();linha - a importação está implícita use. Além disso, sugiro não construir a saída inteira na memória primeiro: basta usar em print;vez de $text .= $_;dentro do loop e soltar o printcomando pós-loop .
mklement0

1

Isso pode ser feito no próprio bash se você tiver controle do formato do arquivo de configuração. Você só precisa originar (".") O arquivo de configuração, em vez de colocá-lo em um subconjunto. Isso garante que as variáveis ​​sejam criadas no contexto do shell atual (e continuem a existir) em vez do subshell (onde a variável desaparece quando o subshell sai).

$ 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

Se o seu arquivo de configuração não puder ser um script de shell, você pode simplesmente 'compilá-lo' antes de executá-lo (a compilação depende do seu 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

No seu caso específico, você pode 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

Então canalize a saída do go.bash no MySQL e pronto, espero que você não destrua seu banco de dados :-).


1
Você não precisa exportar as variáveis ​​do arquivo config.data; basta apenas defini-los. Você também não parece estar lendo o arquivo de modelo em nenhum momento. Ou, talvez, o arquivo de modelo seja modificado e contenha as operações 'eco' ... ou estou perdendo alguma coisa?
6119 Jonathan Leffler

1
Bom ponto de vista das exportações, faço isso por padrão para que estejam disponíveis para subcamadas e isso não causa danos, pois elas morrem quando saem. O arquivo 'template' é o próprio script com suas instruções de eco. Não é necessário introduzir um terceiro arquivo - é basicamente uma operação do tipo mailmerge.
6129

1
O "script em si com suas declarações de eco" não é um modelo: é um script. Pense readibility (e manutenção) diferença entre <tipo xml = "$ TYPE"> e echo '<tipo xml = " '$ TYPE'">'
Pierre-Olivier Vares

1
@ Pierre, não há instruções de eco no meu script de configuração, são apenas exportações, e eu mostrei como você pode evitar isso mesmo com uma quantidade mínima de pré-processamento. Se você está falando sobre a declaração de eco nos meus outros scripts (como go.bash), você tem a ponta errada - eles não fazem parte da solução, são apenas uma maneira de mostrar que as variáveis ​​estão sendo definido corretamente.
22415

1
@paxdiablo: Parece que você esqueceu a pergunta: << Quero canalizar a saída de um arquivo "template" para o MySQL >>. Portanto, o uso de um modelo é a pergunta, não é "o lado errado do bastão". Exportando variáveis e ecoando-los em outro script simplesmente não responder a pergunta em tudo
Pierre-Olivier Vares

0

Edição permanente de arquivos potencialmente múltiplos, com backups.

  perl -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : ""/eg' \
    -i.orig \
    -p config/test/*
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.