Forçando o bash a expandir variáveis ​​em uma string carregada de um arquivo


88

Estou tentando descobrir como fazer bash (forçar?) Expandir variáveis ​​em uma string (que foi carregada de um arquivo).

Eu tenho um arquivo chamado "something.txt" com o conteúdo:

hello $FOO world

Então eu corro

export FOO=42
echo $(cat something.txt)

isso retorna:

   hello $FOO world

Não expandiu $ FOO, embora a variável tenha sido definida. Não consigo avaliar ou fornecer o arquivo - pois ele tentará executá-lo (não é executável como está - só quero a string com as variáveis ​​interpoladas).

Alguma ideia?



Você quis dizer que o comentário era para a resposta abaixo?
Michael Neale

Eu quis dizer o comentário para você. Mais de uma resposta propõe o uso de evale é importante estar ciente das implicações.
Pausado até novo aviso.

@DennisWilliamson entendeu - estou um pouco atrasado para responder, mas boa dica. E, claro, nos anos que se passaram, tivemos coisas como "choque de bomba", então seu comentário resistiu ao teste do tempo! chapéu de gorjeta
Michael Neale

Respostas:


111

Tropecei no que penso ser a resposta a esta pergunta: o envsubstcomando.

envsubst < something.txt

Caso ainda não esteja disponível em sua distro, está no

Pacote GNU gettext.

@Rockallite - Eu escrevi um pequeno script de wrapper para cuidar do problema '\ $'.

(Aliás, há um "recurso" de envsubst, explicado em https://unix.stackexchange.com/a/294400/7088 para expandir apenas algumas das variáveis ​​na entrada, mas concordo que escapar das exceções é muito mais conveniente.)

Aqui está o meu script:

#! /bin/bash
      ## -*-Shell-Script-*-
CmdName=${0##*/}
Usage="usage: $CmdName runs envsubst, but allows '\$' to  keep variables from
    being expanded.
  With option   -sl   '\$' keeps the back-slash.
  Default is to replace  '\$' with '$'
"

if [[ $1 = -h ]]  ;then echo -e >&2  "$Usage" ; exit 1 ;fi
if [[ $1 = -sl ]] ;then  sl='\'  ; shift ;fi

sed 's/\\\$/\${EnVsUbDolR}/g' |  EnVsUbDolR=$sl\$  envsubst  "$@"

5
+1. Esta é a única resposta que garantidamente não fará substituições de comandos, remoção de citações, processamento de argumentos, etc.
Josh Kelley

6
Ele apenas funciona para vars shell totalmente exportados (no bash). por exemplo, S = '$ a $ b'; a = conjunto; exportação b = exportado; echo $ S | envsubst; rendimentos: "exportado"
gaoithe

1
obrigado - acho que depois de todos esses anos, devo aceitar essa resposta.
Michael Neale

1
No entanto, ele não lida com o $escape de cifrão ( ), por exemplo echo '\$USER' | envsubst, produzirá o nome de usuário atual com uma barra invertida como prefixo, não $USER.
Rockallite de

3
parece conseguir isso em um Mac com necessidades de homebrewbrew link --force gettext
KeepCalmAndCarryOn

25

Muitas das respostas usando evale echotipo de trabalho, mas quebram em várias coisas, como várias linhas, tentativa de escape de metacaracteres shell, escapes dentro do modelo que não se destina a ser expandido por bash, etc.

Eu tive o mesmo problema e escrevi esta função shell, que pelo que eu posso dizer, lida com tudo corretamente. Isso ainda removerá apenas as novas linhas finais do modelo, por causa das regras de substituição de comandos do bash, mas nunca achei que isso fosse um problema, desde que tudo o mais permanecesse intacto.

apply_shell_expansion() {
    declare file="$1"
    declare data=$(< "$file")
    declare delimiter="__apply_shell_expansion_delimiter__"
    declare command="cat <<$delimiter"$'\n'"$data"$'\n'"$delimiter"
    eval "$command"
}

Por exemplo, você pode usá-lo assim com um parameters.cfgque é realmente um script de shell que apenas define variáveis ​​e um template.txtque é um modelo que usa essas variáveis:

. parameters.cfg
printf "%s\n" "$(apply_shell_expansion template.txt)" > result.txt

Na prática, eu uso isso como uma espécie de sistema de template leve.


4
Este é um dos melhores truques que encontrei até agora :). Com base em sua resposta, é tão fácil construir uma função que expanda uma variável com uma string contendo referências a outras variáveis ​​- apenas substitua $ data por $ 1 em sua função e vamos lá! Eu acredito que esta é a melhor resposta para a pergunta original, BTW.
galáxia

Até mesmo isso tem evalarmadilhas usuais . Tente adicionar ; $(eval date)no final de qualquer linha dentro do arquivo de entrada e ele executará o datecomando.
anubhava

1
@anubhava não tenho certeza do que você quer dizer; esse é realmente o comportamento de expansão desejado neste caso. Em outras palavras, sim, você deve ser capaz de executar código de shell arbitrário com isso.
wjl

22

podes tentar

echo $(eval echo $(cat something.txt))

9
Use echo -e "$(eval "echo -e \"`<something.txt`\"")"se precisar que novas linhas sejam mantidas.
pevik

Funciona como um encanto
kyb

1
Mas apenas se template.txtfor um texto. Esta operação é perigosa porque executa (avalia) comandos se eles forem.
kyb

2
mas esta aspas duplas removida
Thomas Decaux

A culpa é do subshell.
ingyhere

8

Você não quer imprimir cada linha, você quer avaliá- la para que o Bash possa realizar substituições de variáveis.

FOO=42
while read; do
    eval echo "$REPLY"
done < something.txt

Consulte help evalou o manual do Bash para obter mais informações.


2
evalé perigoso ; você não apenas se $varnameexpande (como faria envsubst), mas $(rm -rf ~)também.
Charles Duffy,

4

Outra abordagem (que parece nojenta, mas estou colocando aqui de qualquer maneira):

Grave o conteúdo de something.txt em um arquivo temporário, com uma instrução echo envolvida nele:

something=$(cat something.txt)

echo "echo \"" > temp.out
echo "$something" >> temp.out
echo "\"" >> temp.out

em seguida, origine-o de volta em uma variável:

RESULT=$(source temp.out)

e $ RESULT terá tudo expandido. Mas parece tão errado!


1
nojento, mas é mais direto, simples e funciona.
kyb

3

Se você deseja apenas que as referências de variáveis ​​sejam expandidas (objetivo que eu tinha para mim), você poderia fazer o seguinte.

contents="$(cat something.txt)"
echo $(eval echo \"$contents\")

(As aspas escapadas em torno de $ contents são a chave aqui)


Eu tentei este, porém o $ () remove terminações de linha no meu caso, o que quebra a string resultante
moz

Mudei a linha eval para eval "echo \"$$contents\""e ela preservou o espaço em branco
moz

1

Solução seguinte:

  • permite a substituição de variáveis ​​que são definidas

  • deixa os marcadores de posição de variáveis ​​inalterados que não são definidos. Isso é especialmente útil durante implantações automatizadas.

  • suporta a substituição de variáveis ​​nos seguintes formatos:

    ${var_NAME}

    $var_NAME

  • relata quais variáveis ​​não são definidas no ambiente e retorna o código de erro para tais casos



    TARGET_FILE=someFile.txt;
    ERR_CNT=0;

    for VARNAME in $(grep -P -o -e '\$[\{]?(\w+)*[\}]?' ${TARGET_FILE} | sort -u); do     
      VAR_VALUE=${!VARNAME};
      VARNAME2=$(echo $VARNAME| sed -e 's|^\${||g' -e 's|}$||g' -e 's|^\$||g' );
      VAR_VALUE2=${!VARNAME2};

      if [ "xxx" = "xxx$VAR_VALUE2" ]; then
         echo "$VARNAME is undefined ";
         ERR_CNT=$((ERR_CNT+1));
      else
         echo "replacing $VARNAME with $VAR_VALUE2" ;
         sed -i "s|$VARNAME|$VAR_VALUE2|g" ${TARGET_FILE}; 
      fi      
    done

    if [ ${ERR_CNT} -gt 0 ]; then
        echo "Found $ERR_CNT undefined environment variables";
        exit 1 
    fi

1
  1. Se something.txt tiver apenas uma linha, um bashmétodo (uma versão mais curta da resposta "icky" de Michael Neale ), usando substituição de processo e comando :

    FOO=42 . <(echo -e echo $(<something.txt))
    

    Resultado:

    hello 42 world
    

    Observe que exportnão é necessário.

  2. Se something.txt tem uma ou mais linhas, um GNU valuate método:sed e

    FOO=42 sed 's/"/\\\"/g;s/.*/echo "&"/e' something.txt
    

1
Achei a sedavaliação que melhor se adequava ao meu propósito. Eu precisava produzir scripts de modelos, que teriam valores preenchidos de variáveis ​​exportadas em outro arquivo de configuração. Ele lida com o escape adequado para a expansão do comando, por exemplo, colocar \$(date)no modelo para obter $(date)a saída. Obrigado!
gadamiak

0
$ eval echo $(cat something.txt)
hello 42 world
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
Copyright (C) 2007 Free Software Foundation, Inc.

0
foo=45
file=something.txt       # in a file is written: Hello $foo world!
eval echo $(cat $file)

1
Obrigado por este trecho de código, que pode fornecer alguma ajuda limitada e imediata. Uma explicação adequada melhoraria muito seu valor a longo prazo, mostrando por que essa é uma boa solução para o problema e a tornaria mais útil para futuros leitores com outras questões semelhantes. Por favor edite sua resposta para adicionar alguma explicação, incluindo as suposições que você fez.
Capricórnio

-1

envsubst é uma ótima solução (veja a resposta de LenW) se o conteúdo que você está substituindo for de tamanho "razoável".

No meu caso, precisei substituir o conteúdo de um arquivo para substituir o nome da variável. envsubstrequer que o conteúdo seja exportado como variáveis ​​de ambiente e o bash tem um problema ao exportar variáveis ​​de ambiente com mais de um megabyte ou mais.

solução awk

Usando a solução da cuonglm de uma questão diferente:

needle="doc1_base64" # The "variable name" in the file. (A $ is not needed.)
needle_file="doc1_base64.txt" # Will be substituted for the needle 
haystack=$requestfile1 # File containing the needle
out=$requestfile2
awk "BEGIN{getline l < \"${needle_file}\"}/${needle}/{gsub(\"${needle}\",l)}1" $haystack > $out

Esta solução funciona até para arquivos grandes.


-3

O seguinte funciona: bash -c "echo \"$(cat something.txt)"\"


Isso não é apenas ineficiente, mas também extremamente perigoso; ele não executa apenas expansões seguras como $foo, mas também inseguras $(rm -rf ~).
Charles Duffy,
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.