Piszę skrypt, który zautomatyzuje tworzenie plików konfiguracyjnych dla Apache i PHP dla mojego własnego serwera WWW. Nie chcę używać żadnych GUI, takich jak CPanel czy ISPConfig.
Mam kilka szablonów plików konfiguracyjnych Apache i PHP. Skrypt Bash musi czytać szablony, dokonywać podstawiania zmiennych i umieszczać przeanalizowane szablony w jakimś folderze. Jaki jest najlepszy sposób, aby to zrobić? Mogę wymyślić kilka sposobów. Który z nich jest najlepszy, czy może są na to lepsze sposoby? Chcę to zrobić w czystym Bashu (na przykład w PHP jest to łatwe)
1) Jak zamienić symbole zastępcze $ {} w pliku tekstowym?
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, jak przekierować tutaj wyjście do pliku zewnętrznego? Czy muszę uciec przed czymś, jeśli zmienne zawierają, powiedzmy, cudzysłowy?
2) Użycie cat & sed do zastąpienia każdej zmiennej jej wartością:
Podany plik template.txt:
The number is ${i}
The word is ${word}
Komenda:
cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"
Wydaje mi się to złe ze względu na konieczność ucieczki przed wieloma różnymi symbolami i przy wielu zmiennych linia będzie zbyt długa.
Czy przychodzi Ci do głowy jakieś inne eleganckie i bezpieczne rozwiązanie?
źródło
Odpowiedzi:
Możesz użyć tego:
perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt
aby zamienić wszystkie
${...}
łańcuchy na odpowiednie zmienne środowiskowe (nie zapomnij wyeksportować ich przed uruchomieniem tego skryptu).W przypadku czystego basha powinno to działać (zakładając, że zmienne nie zawierają łańcuchów $ {...}):
#!/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
. Rozwiązanie, które nie zawiesza się, jeśli RHS odwołuje się do zmiennej, która odwołuje się do siebie:
#!/bin/bash line="$(cat; echo -n a)" end_offset=${#line} while [[ "${line:0:$end_offset}" =~ (.*)(\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do PRE="${BASH_REMATCH[1]}" POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}" VARNAME="${BASH_REMATCH[3]}" eval 'VARVAL="$'$VARNAME'"' line="$PRE$VARVAL$POST" end_offset=${#PRE} done echo -n "${line:0:-1}"
OSTRZEŻENIE : Nie znam sposobu na poprawną obsługę danych wejściowych z wartościami NUL w bashu lub zachowanie liczby końcowych znaków nowej linii. Ostatni wariant jest przedstawiony tak, jak jest, ponieważ powłoki „uwielbiają” wejścia binarne:
read
zinterpretuje ukośniki odwrotne.read -r
nie zinterpretuje odwrotnych ukośników, ale nadal usunie ostatnią linię, jeśli nie kończy się nową linią."$(…)"
obetnie jak wielu nowych linii wahaczami wzdłużnymi, ponieważ nie występują, więc kończy…
się; echo -n a
i stosowanieecho -n "${line:0:-1}"
: to spada ostatniego znaku (co jesta
) i zachowuje jak wielu nowych linii wahaczami wzdłużnymi, jak było na początku (nie tym).źródło
[^}]
się[A-Za-Z_][A-Za-z0-9_]
w wersji bash, aby powłoka z wykraczając poza ścisłym podstawienia (np gdyby próbowali procesu${some_unused_var-$(rm -rf $HOME)}
).$&
w rozwiązaniu perla na""
: najpierw pozostawia${...}
nietknięte, jeśli nie uda się go zastąpić, po drugie zastępuje go pustym ciągiem.while
pętla przeczytała ostatnią linię, nawet jeśli nie jest zakończona nową linią, użyjwhile read -r line || [[ -n $line ]]; do
. Dodatkowo, twojeread
polecenie usuwa początkowe i końcowe spacje z każdego wiersza; aby tego uniknąć, użyjwhile IFS= read -r line || [[ -n $line ]]; do
\
ich unikanie).Próbować
envsubst
FOO=foo BAR=bar export FOO BAR envsubst <<EOF FOO is $FOO BAR is $BAR EOF
źródło
envsubst
nie jest wymagane przy używaniu heredoc, ponieważ bash traktuje heredoc jako dosłowny ciąg w cudzysłowie i interpoluje już w nim zmienne. Jest to jednak doskonały wybór, gdy chcesz odczytać szablon z innego pliku. Dobry zamiennik znacznie bardziej uciążliwychm4
.envsubst
to narzędzie GNU gettext, które w rzeczywistości nie jest aż tak niezawodne (ponieważ gettext jest przeznaczony do lokalizowania ludzkich wiadomości). Co najważniejsze, nie rozpoznaje podstawień $ {VAR} ze znakami ucieczki odwrotnym ukośnikiem (więc nie możesz mieć szablonu, który używa podstawień $ VAR w czasie wykonywania, jak skrypt powłoki lub plik konfiguracyjny Nginx). Zobacz moją odpowiedź na rozwiązanie, które obsługuje ucieczki z ukośnikiem odwrotnym.<<"EOF"
, który nie interpoluje zmiennych (terminatory w cudzysłowach są jak pojedyncze cudzysłowy heredoców).cat template.txt | envsubst
envsubst był dla mnie nowy. Fantastyczny.
Dla przypomnienia, użycie heredoc to świetny sposób na utworzenie szablonu pliku conf.
STATUS_URI="/hows-it-goin"; MONITOR_IP="10.10.2.15"; cat >/etc/apache2/conf.d/mod_status.conf <<EOF <Location ${STATUS_URI}> SetHandler server-status Order deny,allow Deny from all Allow from ${MONITOR_IP} </Location> EOF
źródło
envsubst
bo to uratowało mnie od dodatkowegoapt-get install gettext-base
w moim pliku DockerfileZgadzam się z użyciem seda: jest to najlepsze narzędzie do wyszukiwania / zamiany. Oto moje podejście:
$ 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
źródło
-i
opcję (edytuj pliki w miejscu) podobną do opcji perla . Sprawdź stronę podręcznika swojego seda .Myślę, że eval działa naprawdę dobrze. Obsługuje szablony z podziałami linii, białymi znakami i wszelkiego rodzaju funkcjami bash. Jeśli oczywiście masz pełną kontrolę nad samymi szablonami:
$ cat template.txt variable1 = ${variable1} variable2 = $variable2 my-ip = \"$(curl -s ifconfig.me)\" $ echo $variable1 AAA $ echo $variable2 BBB $ eval "echo \"$(<template.txt)\"" 2> /dev/null variable1 = AAA variable2 = BBB my-ip = "11.22.33.44"
Oczywiście tej metody należy używać ostrożnie, ponieważ eval może wykonać dowolny kod. Uruchomienie tego jako root nie wchodzi w rachubę. Cytaty w szablonie należy uciec, w przeciwnym razie zostaną zjedzone
eval
.Można również użyć tu dokumenty, jeśli wolisz
cat
, abyecho
$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
@plockc dostarczyło rozwiązanie, które pozwala uniknąć problemu z ucieczką cytatów basha:
$ eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
Edycja: Usunięto część dotyczącą uruchamiania tego jako root za pomocą sudo ...
Edycja: Dodano komentarz o tym, jak należy uciekać przed cytatami, dodano rozwiązanie plockc do miksu!
źródło
eval
edecho
/cat
przetwarza je; spróbujeval "echo \"'\$HOME'\""
.Mam rozwiązanie bash takie jak mogsie ale z heredoc zamiast herestring abyś mógł uniknąć unikania podwójnych cudzysłowów
eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
źródło
${param:?}
tekstem zagnieżdżonym wokół parametrów opcjonalnych. Przykład:${DELAY:+<delay>$DELAY</delay>}
rozwija się do niczego, gdy DELAY jest nieokreślona i <delay> 17 </delay>, gdy DELAY = 17._EOF_$$
.$trailing_newline
, lub użycie$NL5
i upewnienie się, że zostanie rozwinięta jako 5 nowych linii.template.txt
, działałoby, aby zachować końcowe znaki nowej linii.eval
, iftemplate.txt
zawieraEOF
w swoim własnym wierszu, przedwcześnie zakończy działanie here-doc, a tym samym przerwie polecenie. (Końcówka kapelusza do @xebeche).Edycja 6 stycznia 2017 r
Musiałem zachować podwójne cudzysłowy w moim pliku konfiguracyjnym, więc podwójne unikanie podwójnych cudzysłowów za pomocą sed pomaga:
render_template() { eval "echo \"$(sed 's/\"/\\\\"/g' $1)\"" }
Nie mogę myśleć o zatrzymywaniu nowych linii końcowych, ale puste linie pomiędzy nimi są zachowywane.
Chociaż to stary temat, IMO znalazłem bardziej eleganckie rozwiązanie tutaj: 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
Wszystkie kredyty dla Grégory'ego Pakosza .
źródło
eval "echo \"$(sed 's/\"/\\"/g' $1)\""
$variables
).Jeśli chcesz używać szablonów Jinja2 , zobacz ten projekt: j2cli .
To wspiera:
źródło
Zamiast wymyślać koło na nowo, idź z envsubst Może być używany w prawie każdym scenariuszu, na przykład przy tworzeniu plików konfiguracyjnych ze zmiennych środowiskowych w kontenerach docker.
Jeśli na komputerze Mac upewnij się, że masz homebrew, połącz go z gettext:
./template.cfg
# We put env variables into placeholders here this_variable_1 = ${SOME_VARIABLE_1} this_variable_2 = ${SOME_VARIABLE_2}
./.env:
./configure.sh
#!/bin/bash cat template.cfg | envsubst > whatever.cfg
Teraz po prostu użyj:
# 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 depends on tools that utilise .env file # automatically like pipenv etc. export SOME_VARIABLE_1 SOME_VARIABLE_2 # Create your config file ./configure.sh
źródło
envsubst
faktycznie działa.envsubst
nie działa na MacOS, trzeba by zainstalować go za pomocą homebrew:brew install gettext
.Zrobiłbym to w ten sposób, prawdopodobnie mniej wydajne, ale łatwiejsze do odczytania / utrzymania.
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
źródło
sed -e 's/VARONE/NEWVALA/g' -e 's/VARTWO/NEWVALB/g' -e 's/VARTHR/NEWVALC/g' < $TEMPLATE > $OUTPUT
Dłuższa, ale solidniejsza wersja zaakceptowanej odpowiedzi:
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
Spowoduje to rozwinięcie wszystkich wystąpień
$VAR
lub${VAR}
do ich wartości środowiskowych (lub, jeśli są one niezdefiniowane, pustego ciągu).Prawidłowo wymyka się odwrotnym ukośnikiem i akceptuje znak $ przed odwrotnym ukośnikiem, aby uniemożliwić podstawianie (w przeciwieństwie do envsubst, który, jak się okazuje, nie robi tego ).
Jeśli więc Twoje środowisko jest:
a twój szablon to:
Two ${TARGET} walk into a \\$FOO. \\\\ \\\$FOO says, "Delete C:\\Windows\\System32, it's a virus." $BAZ replies, "\${NOPE}s."
wynik byłby:
Two backslashes walk into a \bar. \\ \$FOO says, "Delete C:\Windows\System32, it's a virus." kenny replies, "${NOPE}s."
Jeśli chcesz usunąć tylko ukośniki odwrotne przed $ (możesz napisać „C: \ Windows \ System32” w szablonie bez zmian), użyj tej nieznacznie zmodyfikowanej wersji:
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
źródło
Oto kolejne czyste rozwiązanie bash:
$ cat code
#!/bin/bash LISTING=$( ls ) cat_template() { echo "cat << EOT" cat "$1" echo EOT } cat_template template | LISTING="$LISTING" bash
$ cat template
(z końcowymi znakami nowej linii i podwójnymi cudzysłowami)<html> <head> </head> <body> <p>"directory listing" <pre> $( echo "$LISTING" | sed 's/^/ /' ) <pre> </p> </body> </html>
wynik
<html> <head> </head> <body> <p>"directory listing" <pre> code template <pre> </p> </body> </html>
źródło
Biorąc odpowiedź z ZyX przy użyciu czystego basha, ale z nowym dopasowaniem wyrażeń regularnych i pośrednim zastępowaniem parametrów, wygląda to następująco:
#!/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
źródło
Jeśli używanie Perla jest opcją i jesteś zadowolony z opierania rozszerzeń wyłącznie na zmiennych środowiskowych (w przeciwieństwie do wszystkich zmiennych powłoki ), rozważ solidną odpowiedź Stuarta P. Bentleya .
Ta odpowiedź ma na celu dostarczenie rozwiązania opartego tylko na bash, które - pomimo użycia
eval
- powinno być bezpieczne w użyciu .Te cele są następujące:
${name}
i$name
zmiennych$(...)
i starsza składnia`...`
)$((...))
i starsza składnia$[...]
).\
(\${name}
)."
i\
przypadkach.Funkcja
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' '`([' }
Przykłady:
$ 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
${HOME:0:10}
, o ile nie zawierają one osadzonego polecenia lub podstawień arytmetycznych, takich jak${HOME:0:$(echo 10)}
$(
i`
instancje są ślepo uciekane).${HOME
(brak zamknięcia}
) BREAK funkcja.\$name
zapobiega ekspansji.\
po którym nie następuje,$
jest zachowywany bez zmian.\
instancji, musisz je podwoić ; na przykład:\\
->\
- to samo co just\
\\\\
->\\
0x1
,0x2
,0x3
.eval
.Jeśli szukasz bardziej restrykcyjnego rozwiązania, które obsługuje tylko
${name}
rozszerzenia - tj. Z obowiązkowymi nawiasami klamrowymi, ignorując$name
odniesienia - zobacz moją odpowiedź .Oto ulepszona wersja
eval
rozwiązania opartego tylko na bash, wolnego od zaakceptowanej odpowiedzi :Ulepszenia to:
${name}
do$name
zmiennych, jak i zmiennych.\
odwołań do zmiennych, które nie powinny być rozwijane.eval
powyższego rozwiązania opartego naIFS= 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, i.e., 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"
źródło
Oto inne rozwiązanie: wygeneruj skrypt bash ze wszystkimi zmiennymi i zawartością pliku szablonu, skrypt ten wyglądałby tak:
word=dog i=1 cat << EOF the number is ${i} the word is ${word} EOF
Gdybyśmy wprowadzili ten skrypt do basha, dałby on pożądane wyjście:
Oto jak wygenerować ten skrypt i przesłać go do basha:
( # Variables echo word=dog echo i=1 # add the template echo "cat << EOF" cat template.txt echo EOF ) | bash
Dyskusja
cat
polecenie za pomocą HEREDOCJeśli chcesz przekierować to wyjście do pliku, zamień ostatnią linię na:
źródło
Ta strona opisuje odpowiedź z awk
awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
źródło
Idealna obudowa do shtpl . (mój projekt, więc nie jest powszechnie używany i brakuje mu dokumentacji. Ale oto rozwiązanie, które oferuje. Może zechcesz go przetestować.)
Po prostu wykonaj:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
Wynik to:
Baw się dobrze.
źródło
# 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" }
Jest to czysta funkcja bash, którą można dostosować do własnych upodobań, używana w produkcji i nie powinna się zepsuć na żadnym wejściu. Jeśli się zepsuje - daj mi znać.
źródło
Możesz także użyć bashible (który wewnętrznie wykorzystuje podejście oceniające opisane powyżej / poniżej).
Oto przykład, jak wygenerować kod HTML z wielu części:
https://github.com/mig1984/bashible/tree/master/examples/templates
źródło
Oto funkcja bash, która zachowuje białe znaki:
# Render a file in bash, i.e. expand environment variables. Preserves whitespace. function render_file () { while IFS='' read line; do eval echo \""${line}"\" done < "${1}" }
źródło
Oto zmodyfikowany
perl
skrypt oparty na kilku innych odpowiedziach:perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template
Funkcje (w zależności od moich potrzeb, ale powinny być łatwe do modyfikacji):
źródło
Spójrz na prosty skrypt pythona do podstawiania zmiennych tutaj: https://github.com/jeckep/vsubst
Jest bardzo prosty w użyciu:
źródło
Aby kontynuować odpowiedź plockca na tej stronie, oto wersja odpowiednia dla myślnika, dla tych z Was, którzy chcą uniknąć bashizmów.
eval "cat <<EOF >outputfile $( cat template.in ) EOF " 2> /dev/null
źródło