Bash Templating: come creare file di configurazione da modelli con Bash?

Sto scrivendo uno script per automatizzare la creazione di file di configurazione per Apache e PHP per il mio server web. Non voglio usare alcuna GUI come CPanel o ISPConfig.

Ho alcuni modelli di file di configurazione Apache e PHP. Lo script di Bash deve leggere i modelli, creare sostituzioni variabili e generare modelli analizzati in una cartella. Qual è il modo migliore per farlo? Posso pensare a diversi modi. Qual è il migliore o potrebbe esserci qualche modo migliore per farlo? Voglio farlo in puro Bash (è facile in PHP per esempio)

1) Come sostituire $ {} segnaposti in un file di testo?

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" 

    Come faccio a redirect l’output in un file esterno qui? Devo sfuggire a qualcosa se le variabili contengono, ad esempio, virgolette?

    2) Usare cat & sed per sostituire ogni variabile con il suo valore:

    Dato template.txt:

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

    Comando:

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

    Mi sembra male a causa della necessità di sfuggire a molti simboli diversi e con molte variabili la linea sarà troppo lunga.

    Puoi pensare ad un’altra soluzione elegante e sicura?

    Puoi usare questo:

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

    per sostituire tutte le stringhe ${...} con le variabili di ambiente corrispondenti (non dimenticare di esportarle prima di eseguire questo script).

    Per puro bash dovrebbe funzionare (assumendo che le variabili non contengano stringhe $ {...}):

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

    . Soluzione che non si blocca se RHS fa riferimento a una variabile che si riferisce a se stessa:

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

    ATTENZIONE : non conosco un modo per gestire correttamente l'input con NUL in bash o preservare la quantità di newline finali. L'ultima variante è presentata così com'è perché le shell "amano" l'input binario:

    1. read interpreterà i backslash.
    2. read -r non interpreterà i backslash, ma continuerà a rilasciare l'ultima riga se non termina con una newline.
    3. "$(…)" sposterà tante nuove righe finali come sono presenti, quindi finisco con ; echo -na ; echo -na e usa echo -n "${line:0:-1}" : questo elimina l'ultimo carattere (che è a ) e conserva il numero di righe finali finali quante erano nell'input (incluso no).

    Prova envsubst

     FOO=foo BAR=bar export FOO BAR envsubst < 

    envsubst era nuovo per me. Fantastico.

    Per la cronaca, l’utilizzo di un heredoc è un ottimo modo per creare un modello di file 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 

    Sono d’accordo con l’utilizzo di sed: è lo strumento migliore per la ricerca / sostituzione. Ecco il mio approccio:

     $ 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 

    Penso che eval funzioni davvero bene. Gestisce i modelli con interruzioni di riga, spazi bianchi e tutti i tipi di cose bash. Se hai il pieno controllo sui modelli stessi, ovviamente:

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

    Questo metodo dovrebbe essere usato con caucanvas, ovviamente, poiché eval può eseguire codice arbitrario. L’esecuzione di questo come root è praticamente fuori questione. Le virgolette nel modello devono essere sfuggite, altrimenti saranno mangiate dalla eval .

    Puoi anche usare i documenti qui se preferisci il cat echo

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

    @plockc ha dimostrato una soluzione che evita il problema di escape di bash:

     $ eval "cat < /dev/null 

    Modifica: parte rimossa relativa all’esecuzione di questo come root usando sudo …

    Modifica: Aggiunto commento su come le virgolette devono essere sfuggite, aggiunta la soluzione di plockc al mix!

    Modifica 6 gennaio 2017

    Avevo bisogno di mantenere le doppie virgolette nel mio file di configurazione in modo da evitare le doppie virgolette di escape con sed:

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

    Non riesco a pensare di mantenere le nuove linee finali, ma vengono mantenute le linee vuote intermedie.


    Anche se è un vecchio argomento, IMO ho trovato una soluzione più elegante qui: 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 

    Tutti i crediti per Grégory Pakosz .

    Ho una soluzione bash come mogsie ma con heredoc invece di herestring per permetterti di evitare le doppie virgolette

     eval "cat < /dev/null 

    L’avrei fatto in questo modo, probabilmente meno efficiente, ma più facile da leggere / mantenere.

     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 

    Una versione più lunga ma più robusta della risposta accettata:

     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 

    Questo espande tutte le istanze di $VAR o ${VAR} ai loro valori di ambiente (o, se sono indefiniti, la stringa vuota).

    Esegue correttamente l’escape dei backslash e accetta un backslash-escape di $ per inibire la sostituzione (diversamente da envsubst, che, a quanto pare, non lo fa ).

    Quindi, se il tuo ambiente è:

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

    e il tuo modello è:

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

    il risultato sarebbe:

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

    Se vuoi solo uscire dai backslash prima di $ (potresti scrivere “C: \ Windows \ System32” in un modello invariato), usa questa versione leggermente modificata:

     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 si desidera utilizzare i modelli Jinja2 , vedere questo progetto: j2cli .

    Supporta:

    • Modelli da file JSON, INI, YAML e flussi di input
    • Templatura da variabili di ambiente

    Prendendo la risposta da ZyX usando bash puro, ma con il nuovo stile regex matching e la sostituzione dei parametri indiretti diventa:

     #!/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 l’uso di Perl è un’opzione e tu sei contento di basare le espansioni solo sulle variabili d’ ambiente (al contrario di tutte le variabili di shell ), considera la robusta risposta di Stuart P. Bentley .

    Questa risposta mira a fornire una soluzione bash-only che, nonostante l’uso di eval , dovrebbe essere sicuro da usare .

    Gli obiettivi sono:

    • Supporta l’espansione di ${name} e $name riferimenti variabili.
    • Prevenire tutte le altre espansioni:
      • sostituzioni di comando ( $(...) e syntax legacy `...` )
      • sostituzioni aritmetiche ( $((...)) e syntax legacy $[...] ).
    • Consenti la soppressione selettiva dell’espansione delle variabili con il prefisso con \ ( \${name} ).
    • Conserva caratteri speciali. nell’input, in particolare " and \ instances”.
    • Consentire l’input tramite argomenti o tramite stdin.

    Funzione 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' '`([' } 

    Esempi:

     $ 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 
    • Per motivi di prestazioni, la funzione legge l'input dello stdin tutto in una volta in memoria, ma è facile adattare la funzione a un approccio line-by-line.
    • Supporta anche espansioni di variabili non base come ${HOME:0:10} , a condizione che non contengano comandi incorporati o sostituzioni aritmetiche, come ${HOME:0:$(echo 10)}
      • Tali sostituzioni incorporate effettivamente BREAK la funzione (perché tutti $( e ` istanze sono sfuggite alla cieca).
      • Allo stesso modo, i riferimenti alle variabili malformati come ${HOME (missing closing } ) BREAK la funzione.
    • A causa della gestione delle stringhe con doppie virgolette, i backslash vengono gestiti come segue:
      • \$name impedisce l'espansione.
      • Un singolo \ non seguito da $ viene conservato così com'è.
      • Se vuoi rappresentare più istanze adiacenti , devi raddoppiarle ; per esempio:
        • \\ -> \ - lo stesso di just \
        • \\\\ -> \\
      • L'input non deve contenere i seguenti caratteri (usati raramente), che vengono utilizzati per scopi interni: 0x1 , 0x2 , 0x3 .
    • C'è una preoccupazione in gran parte ipotetica che se bash dovesse introdurre una nuova syntax di espansione, questa funzione potrebbe non impedire tali espansioni - vedi sotto per una soluzione che non usa eval .

    Se stai cercando una soluzione più restrittiva che supporti solo espansioni ${name} - cioè, con parentesi graffe obbligatorie , ignorando $name riferimenti $name - guarda questa mia risposta .


    Ecco una versione migliorata della soluzione di sola eval bash dalla risposta accettata :

    I miglioramenti sono:

    • Supporto per l'espansione di ${name} e $name variabile.
    • Supporto per \ -escaping di riferimenti alle variabili che non dovrebbero essere espansi.
    • A differenza della soluzione basata su eval sopra,
      • le espansioni non base sono ignorate
      • riferimenti alle variabili non validi vengono ignorati (non interrompono lo 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" 

    Questa pagina descrive una risposta con awk

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

    Custodia perfetta per shtpl . (progetto mio, quindi non è ampiamente in uso e manca di documentazione.) Ma ecco la soluzione che offre comunque. Puoi volerlo testare.)

    Basta eseguire:

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

    Il risultato è:

     the number is 1 the word is dog 

    Divertiti.

    Ecco un’altra soluzione bash pura:

    • sta usando heredoc, quindi:
      • la complessità non aumenta a causa della syntax aggiuntiva richiesta
      • il modello può includere codice bash
        • ciò ti consente anche di rielaborare le cose correttamente. Vedi sotto.
    • non usa eval, quindi:
      • nessun problema con il rendering delle righe vuote finali
      • nessun problema con le virgolette nel modello

    $ cat code

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

    $ cat template (con newlines finali e doppi apici)

         

    "directory listing"

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

    produzione

         

    "directory listing"

     code template 
     

    Ecco un’altra soluzione: generare uno script bash con tutte le variabili e il contenuto del file di modello, tale script sarebbe simile a questo:

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

    Se alimentiamo questo script in bash, produrrebbe l'output desiderato:

     the number is 1 the word is dog 

    Ecco come generare quello script e alimentare quello script in bash:

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

    Discussione

    • Le parentesi aprono una sub shell, il suo scopo è quello di raggruppare tutti gli output generati
    • All'interno della sub shell, generiamo tutte le dichiarazioni delle variabili
    • Anche nella sub shell, generiamo il comando cat con HEREDOC
    • Infine, alimentiamo l'output della sub shell in bash e produciamo l'output desiderato
    • Se desideri redirect questo output in un file, sostituisci l'ultima riga con:

       ) | bash > output.txt 

    È inoltre ansible utilizzare bashible (che utilizza internamente l’approccio di valutazione descritto sopra / sotto).

    C’è un esempio, come generare un codice HTML da più parti:

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

    Questa è la pura funzione di bash regolabile a tuo piacimento, utilizzata in produzione e che non dovrebbe interrompere alcun input. Se si rompe, fammi sapere.

    Ecco una funzione di bash che preserva gli spazi:

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

    Ecco uno script perl modificato basato su alcune delle altre risposte:

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

    Funzionalità (in base alle mie esigenze, ma dovrebbero essere facili da modificare):

    • Salta le espansioni dei parametri sfuggite (ad es. \ $ {VAR}).
    • Supporta espansioni parametro del modulo $ {VAR}, ma non $ VAR.
    • Sostituisce $ {VAR} con una stringa vuota se non c’è VAR envar.
    • Supporta solo az, AZ, 0-9 e caratteri di sottolineatura nel nome (escluse le cifre nella prima posizione).

    Invece di reinventare la ruota, vai con envsubst Può essere utilizzato in quasi tutti gli scenari, ad esempio creando file di configurazione da variabili di ambiente in contenitori docker.

    Se su Mac assicurati di avere homebrew quindi collegalo da 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 

    Ora basta usarlo:

     # 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