Bash Templating: Como construir arquivos de configuração a partir de templates com o Bash?

Eu estou escrevendo um script para automatizar a criação de arquivos de configuração para o Apache e PHP para o meu próprio servidor. Eu não quero usar nenhuma GUI como CPanel ou ISPConfig.

Eu tenho alguns modelos de arquivos de configuração do Apache e do PHP. O script Bash precisa ler modelos, fazer a substituição de variables ​​e enviar modelos analisados ​​em alguma pasta. Qual o melhor jeito pra fazer isso? Eu posso pensar em várias maneiras. Qual deles é o melhor ou pode haver algumas maneiras melhores de fazer isso? Eu quero fazer isso em Bash puro (é fácil no PHP por exemplo)

1) Como replace $ {} espaços reservados em um arquivo 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" 

BTW, como faço para redirect a saída para o arquivo externo aqui? Preciso escaping alguma coisa se as variables ​​contiverem, por exemplo, citações?

2) Usando cat & sed para replace cada variável pelo seu valor:

Dado template.txt:

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

Comando:

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

Parece ruim para mim por causa da necessidade de escaping de muitos símbolos diferentes e com muitas variables ​​a linha será muito longa.

Você consegue pensar em alguma outra solução elegante e segura?

Você pode usar isto:

 perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt 

para replace todas as strings ${...} por variables ​​de ambiente correspondentes (não esqueça de exportá-las antes de executar este script).

Para o bash puro, isso deve funcionar (supondo que as variables ​​não contenham $ {...} strings):

 #!/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 

. Solução que não trava se o RHS fizer referência a alguma variável que faça referência a si mesma:

 #! / bin / bash
 line = "$ (cat; echo -na)"
 end_offset = $ {# line}
 while [["$ {line: 0: $ end_offset}" = ~ (. *) (\ $ \ {([a-zA-Z _] [a-zA-Z_0-9] *) \}) (. * )]];  Faz
     PRE = "$ {BASH_REMATCH [1]}"
     POST = "$ {BASH_REMATCH [4]} $ {line: $ end_offset: $ {# linha}}"
     VARNAME = "$ {BASH_REMATCH [3]}"
     eval 'VARVAL = "$' $ VARNAME '"'
     line = "$ PRE $ VARVAL $ POST"
     end_offset = $ {# PRE}
 feito
 echo -n "$ {line: 0: -1}"

AVISO : Eu não sei uma maneira de manipular corretamente a input com NULs no bash ou preservar a quantidade de novas linhas finais. A última variante é apresentada porque as shells “love” input binário:

  1. read interpretará as barras invertidas.
  2. read -r não interpretará as barras invertidas, mas ainda assim a última linha será descartada se não terminar com uma nova linha.
  3. "$(…)" irá remover tantas linhas novas quantas estiverem presentes, então eu termino com ; echo -na ; echo -na e use echo -n "${line:0:-1}" : isso elimina o último caractere (que é a ) e preserva o número de novas linhas à direita da input (incluindo no).

Tente envsubst

 FOO=foo BAR=bar export FOO BAR envsubst <  

envsubst era novo para mim. Fantástico.

Para o registro, usar um heredoc é uma ótima maneira de modelar um arquivo 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 

Concordo em usar o sed: é a melhor ferramenta para pesquisar / replace. Aqui está minha abordagem:

 $ 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 

Eu acho que eval funciona muito bem. Ele lida com modelos com quebras de linha, espaços em branco e todos os tipos de coisas. Se você tem controle total sobre os modelos, é claro:

 $ cat template.txt variable1 = ${variable1} variable2 = $variable2 my-ip = \"$(curl -s ifconfig.me)\" $ echo $variable1 AAA $ echo $variable2 BBB $ eval "echo \"$( 

Esse método deve ser usado com cuidado, é claro, já que o eval pode executar código arbitrário. Executar isso como root está praticamente fora de questão. As citações no modelo precisam ser escapadas, caso contrário, elas serão comidas por eval .

Você também pode usar aqui documentos, se você preferir cat para echo

 $ eval "cat < << \"$( /dev/null 

@plockc provodou uma solução que evita o problema de escape de cotações básicas:

 $ eval "cat <  /dev/null 

Edit: Removido parte sobre como executar isso como root usando sudo …

Edit: Adicionado comentário sobre como aspas precisam ser escapadas, acrescentou a solução do plockc para a mistura!

Editar 6 de janeiro de 2017

Eu precisava manter aspas duplas no meu arquivo de configuração para que duplas aspas duplas com sed ajudassem:

 render_template() { eval "echo \"$(sed 's/\"/\\\\"/g' $1)\"" } 

Eu não consigo pensar em seguir novas linhas, mas linhas vazias entre elas são mantidas.


Embora seja um tópico antigo, IMO descobri solução mais elegante aqui: 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 os créditos para Grégory Pakosz .

Eu tenho uma solução bash como mogsie mas com heredoc em vez de herestring para permitir que você evite escaping aspas duplas

 eval "cat <  /dev/null 

Eu teria feito dessa maneira, provavelmente menos eficiente, mas mais fácil de ler / manter.

 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 

Uma versão mais longa, porém mais robusta, da resposta aceita:

 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 

Isso expande todas as instâncias de $VAR ou ${VAR} para seus valores de ambiente (ou, se forem indefinidos, a cadeia vazia).

Ele escapa adequadamente das barras invertidas e aceita um $ escapado com contrabaras para inibir a substituição (diferentemente do envsubst, que, ao que parece , não faz isso ).

Então, se o seu ambiente é:

 FOO=bar BAZ=kenny TARGET=backslashes NOPE=engi 

e seu modelo é:

 Two ${TARGET} walk into a \\$FOO. \\\\ \\\$FOO says, "Delete C:\\Windows\\System32, it's a virus." $BAZ replies, "\${NOPE}s." 

o resultado seria:

 Two backslashes walk into a \bar. \\ \$FOO says, "Delete C:\Windows\System32, it's a virus." kenny replies, "${NOPE}s." 

Se você quiser apenas escaping barras invertidas antes de $ (você pode escrever “C: \ Windows \ System32” em um modelo inalterado), use esta versão ligeiramente 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 

Se você quiser usar modelos Jinja2 , veja este projeto: j2cli .

Suporta:

  • Modelos de arquivos JSON, INI, YAML e streams de input
  • Templating de variables ​​de ambiente

Tomando a resposta do ZyX usando o bash puro, mas com o novo estilo regex matching e a substituição indireta de parâmetros, ele se torna:

 #!/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 

Se usar Perl for uma opção e você se contentar em basear expansões apenas em variables ​​de ambiente (ao contrário de todas as variables ​​de shell ), considere a resposta robusta de Stuart P. Bentley .

Esta resposta visa fornecer uma solução somente bash que – apesar do uso de eval – deve ser segura de usar .

Os objectives são:

  • Suporta a expansão de referências a variables ${name} e $name .
  • Impedir todas as outras expansões:
    • substituições de comandos ( $(...) e syntax legada `...` )
    • substituições aritméticas ( $((...)) e syntax legada $[...] ).
  • Permitir a supressão seletiva da expansão de variables ​​prefixando com \ ( \${name} ).
  • Preserve caracteres especiais. na input, notavelmente " e \ instâncias.
  • Permitir input via argumentos ou via stdin.

Função 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' '`([' } 

Exemplos:

 $ 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 motivos de desempenho, a function lê a input de stdin toda de uma vez na memory, mas é fácil adaptar a function a uma abordagem linha por linha.
  • Também suporta expansões de variables não básicas , como ${HOME:0:10} , desde que não contenham nenhum comando incorporado ou substituições aritméticas, como ${HOME:0:$(echo 10)}
    • Essas substituições embutidas, na verdade, quebram a function (porque todas as instâncias $( e ` são escapadas cegamente).
    • Da mesma forma, referências variables ​​malformadas como ${HOME (falta de fechamento } ) BREAK a function.
  • Devido à manipulação do bash de cadeias duplas entre aspas, as barras invertidas são manipuladas da seguinte maneira:
    • \$name impede a expansão.
    • Um único \ não seguido por $ é preservado como está.
    • Se você quiser representar várias instâncias adjacentes \ , você deve dobrá-las ; por exemplo:
      • \\ -> \ - o mesmo que apenas \
      • \\\\ -> \\
    • A input não deve conter os seguintes caracteres (raramente usados), que são usados ​​para propósitos internos: 0x1 , 0x2 , 0x3 .
  • Existe uma grande preocupação hipotética de que se o bash deve introduzir uma nova syntax de expansão, esta function pode não impedir tais expansões - veja abaixo uma solução que não usa eval .

Se você está procurando por uma solução mais restritiva que suporte apenas expansões ${name} - ou seja, com chaves obrigatórias , ignorando as referências $name - veja esta minha resposta .


Aqui está uma versão melhorada da solução apenas bash, eval free da resposta aceita :

As melhorias são:

  • Suporte para expansão de referências a variables ${name} e $name .
  • Suporte para \ -escaping de referências de variables ​​que não devem ser expandidas.
  • Ao contrário da solução baseada em eval acima,
    • expansões não básicas são ignoradas
    • referências de variables ​​malformadas são ignoradas (elas não quebram o 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, 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 descreve uma resposta com o awk

 awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt 

Caso perfeito para shtpl . (projeto meu, então não é amplamente usado e não tem documentação. Mas aqui está a solução que ele oferece de qualquer maneira. Você pode querer testá-lo.)

Apenas execute:

 $ i=1 word=dog sh -c "$( shtpl template.txt )" 

Resultado é:

 the number is 1 the word is dog 

Diverta-se.

Aqui está outra solução pura:

  • está usando heredoc, então:
    • complexidade não aumenta por causa da syntax adicional necessária
    • template pode include código bash
      • Isso também permite recuar as coisas corretamente. Ver abaixo.
  • não usa eval, então:
    • sem problemas com a renderização de linhas vazias à direita
    • sem problemas com cotações no modelo

$ cat code

 #!/bin/bash LISTING=$( ls ) cat_template() { echo "cat < < EOT" cat "$1" echo EOT } cat_template template | LISTING="$LISTING" bash 

$ cat template (com novas linhas e aspas duplas)

     

"directory listing"

 $( echo "$LISTING" | sed 's/^/ /' ) 
 

saída

     

"directory listing"

 code template 
 

Aqui está outra solução: gerar um script bash com todas as variables ​​e o conteúdo do arquivo de modelo, esse script ficaria assim:

 word=dog i=1 cat < < EOF the number is ${i} the word is ${word} EOF 

Se nós alimentarmos este script no bash, produziríamos a saída desejada:

 the number is 1 the word is dog 

Aqui está como gerar esse script e alimentar esse script no bash:

 ( # Variables echo word=dog echo i=1 # add the template echo "cat < < EOF" cat template.txt echo EOF ) | bash 

Discussão

  • Os parênteses abrem um sub shell, sua finalidade é agrupar toda a saída gerada
  • Dentro do sub shell, geramos todas as declarações de variables
  • Também no sub shell, geramos o comando cat com o HEREDOC
  • Finalmente, nós alimentamos a saída do sub shell para bash e produzimos a saída desejada
  • Se você quiser redirect essa saída para um arquivo, substitua a última linha por:

     ) | bash > output.txt 

Você também pode usar o bashible (que usa internamente a abordagem de avaliação descrita acima / abaixo).

Há um exemplo, como gerar um HTML de várias 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 é a function bash pura ajustável ao seu gosto, usada na produção e não deve quebrar em nenhuma input. Se quebrar, me avise.

Aqui está uma function bash que preserva o espaço em branco:

 # Render a file in bash, ie expand environment variables. Preserves whitespace. function render_file () { while IFS='' read line; do eval echo \""${line}"\" done < "${1}" } 

Aqui está um script perl modificado baseado em algumas das outras respostas:

 perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template 

Recursos (com base nas minhas necessidades, mas devem ser fáceis de modificar):

  • Ignora expansões de parâmetros com escape (por exemplo, \ $ {VAR}).
  • Suporta expansões de parâmetros do formulário $ {VAR}, mas não $ VAR.
  • Substitui $ {VAR} por uma string em branco se não houver nenhum envar VAR.
  • Suporta apenas az, AZ, 0-9 e caracteres de sublinhado no nome (excluindo dígitos na primeira posição).

Em vez de reinventar a roda, vá com envsubst Pode ser usado em quase todos os cenários, por exemplo, construindo arquivos de configuração de variables ​​de ambiente em contêineres do Docker.

Se no mac, certifique-se de ter homebrew, então vincule-o ao 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 

Agora é só usá-lo:

 # 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