Bash Templating: как построить конфигурационные файлы из шаблонов с помощью Bash?
Я пишу скрипт для автоматизации создания конфигурационных файлов для Apache и PHP для моего собственного веб-сервера. Я не хочу использовать какие-либо графические интерфейсы, такие как CPanel или ISPConfig.
у меня есть несколько шаблонов файлов конфигурации Apache и PHP. Скрипт Bash должен читать шаблоны, делать подстановку переменных и выводить разбираемые шаблоны в какую-то папку. Каков наилучший способ сделать это? Я могу придумать несколько способов. Какой из них лучше или может быть есть несколько лучших способов сделать это? Я хочу чтобы сделать это в чистом Bash (это легко в PHP, например)
1) как заменить $ {} заполнители в текстовом файле?
шаблон.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"
кстати, как мне перенаправить вывод на внешний файл здесь? Нужно ли мне что-то экранировать, если переменные содержат, скажем, кавычки?
2) Использование cat & sed для замены каждой переменной ее значением:
дали шаблон.txt:
The number is ${i}
The word is ${word}
:
cat template.txt | sed -e "s/${i}/1/" | sed -e "s/${word}/dog/"
мне кажется плохим из-за необходимости избегать многих разных символов и со многими переменными линия будет слишком длинной.
можете ли вы придумать какое-то другое элегантное и безопасное решение?
21 ответ:
вы можете использовать это:
perl -p -i -e 's/$\{([^}]+)\}/defined $ENV{} ? $ENV{} : $&/eg' < template.txt
заменить все
${...}
строки с соответствующими переменными окружения (не забудьте экспортировать их перед запуском этого скрипта).для чистого bash это должно работать (предполагая, что переменные не содержат ${...} строки):
#!/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
. Решение, которое не зависает, если RHS ссылается на некоторую переменную, которая ссылается на себя:
#!/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}"предупреждение: я не знаю как правильно обрабатывать ввод с нулями в bash или сохранять количество завершающих новых строк. Последний вариант представлен так, как он есть, потому что оболочки "любят" двоичный вход:
read
будет интерпретировать обратные косые черты.read -r
не будет интерпретировать обратные косые черты, но все равно отбросит последнюю строку, если она не заканчивается новой строкой."$(…)"
будет лишать столько конечных новых строк, сколько есть, поэтому я заканчиваю…
С; echo -n a
и использоватьecho -n "${line:0:-1}"
: это отбрасывает последний символ (который являетсяa
) и сохраняет столько конечных новых строк, сколько было во входных данных (включая no).
envsubst был новым для меня. Фантастический.
для записи использование heredoc-отличный способ создать шаблон файла 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
Я согласен с использованием sed: это лучший инструмент для поиска/замены. Вот мой подход:
$ 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
Я думаю, что eval работает очень хорошо. Он обрабатывает шаблоны с разрывами линий, пробелами и всевозможными вещами bash. Если у вас есть полный контроль над самими шаблонами, конечно:
$ 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"
этот метод следует использовать с осторожностью, конечно, так как eval может выполнять произвольный код. Запуск этого как root в значительной степени не может быть и речи. Кавычки в шаблоне нужно экранировать, иначе они будут съедены
eval
.вы также можете использовать здесь документы, если вы предпочитаете
cat
доecho
$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
@plockc provoded решение, которое позволяет избежать проблемы с цитатой bash:
$ eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
Edit: удалена часть о запуске этого как root с помощью sudo...
Edit: добавлен комментарий о том, как кавычки должны быть экранированы, добавлено решение plockc в микс!
Редактировать 6 Января 2017 Года
мне нужно было сохранить двойные кавычки в моем конфигурационном файле, поэтому двойное экранирование двойных кавычек с помощью sed помогает:
render_template() { eval "echo \"$(sed 's/\"/\\"/g' )\"" }
Я не могу думать о сохранении трейлинг новых строк, но пустые строки между ними сохраняются.
хотя это старая тема, ИМО я нашел более элегантное решение здесь: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 )\"" } user="Gregory" render_template /path/to/template.txt > path/to/configuration_file
все кредиты на Pakosz Грегори.
У меня есть решение bash, как mogsie, но с heredoc вместо herestring, чтобы позволить вам избежать избежания двойных кавычек
eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
более длинная, но более надежная версия принятого ответа:
perl -pe 's;(\*)($([a-zA-Z_][a-zA-Z_0-9]*)|$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr(,0,int(length()/2)).(&&length()%2?:$ENV{||});eg' template.txt
это расширяет все экземпляры
$VAR
или${VAR}
к их значениям среды (или, если они не определены, пустая строка).он правильно избегает обратной косой черты и принимает обратную косую черту-escaped $, чтобы заблокировать замену (в отличие от envsubst, который, оказывается,не делает этого).
Итак, если ваше окружение это:
FOO=bar BAZ=kenny TARGET=backslashes NOPE=engi
и ваш шаблон:
Two ${TARGET} walk into a \$FOO. \\ \$FOO says, "Delete C:\Windows\System32, it's a virus." $BAZ replies, "${NOPE}s."
результат будет такой:
Two backslashes walk into a \bar. \ $FOO says, "Delete C:\Windows\System32, it's a virus." kenny replies, "${NOPE}s."
если вы только хотите избежать обратной косой черты перед $ (Вы можете написать "C:\Windows\System32" в шаблоне без изменений), используйте эту слегка измененную версию:
perl -pe 's;(\*)($([a-zA-Z_][a-zA-Z_0-9]*)|$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr(,0,int(length()/2)).(length()%2?:$ENV{||});eg' template.txt
Я бы сделал это таким образом, вероятно, менее эффективным, но легче читать/поддерживать.
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
принимая ответ от ZyX, используя чистый bash, но с новым соответствием регулярному выражению стиля и косвенной заменой параметров он становится:
#!/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
при использовании Perl это вариант, и вы довольны базированием расширений на окружающая среда переменные (в отличие от всех shell переменные), считают надежный ответ Стюарта П. Бентли.
этот ответ направлен на обеспечение bash-только решение что - несмотря на использование
eval
- должно быть безопасно для использования.в цели являются:
- поддержка расширения обоих
${name}
и$name
ссылки на переменные.- предотвратить все другие расширения:
- команды заменами (
$(...)
и устаревший синтаксис`...`
)- арифметические подстановки (
$((...))
и устаревший синтаксис$[...]
).- разрешить выборочное подавление переменного расширения путем префикса с
\
(${name}
).- сохранить специальные символы. во входных данных, в частности
"
и\
экземпляров.- разрешить ввод либо через аргументы, либо через stdin.
функции
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 '`([' '') # 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 '' '`([' }
примеры:
$ 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
- по соображениям производительности функция считывает вход stdin все сразу в память,но легко приспособить функцию к построчному подходу.
- также поддерживает небазовые переменные расширения, такие как
${HOME:0:10}
, если они не содержат встроенных команд или арифметических подстановок, таких как${HOME:0:$(echo 10)}
- такие встроенные замены фактически нарушают функцию (потому что все
$(
и`
экземпляры слепо бежал).- аналогично, искаженные ссылки на переменные, такие как
${HOME
(отсутствует закрывающий тег}
) сломать функцию.- из-за обработки Баша строки в двойных кавычках, обратные косые черты обрабатываются следующим образом:
$name
предотвращает расширение.- один
\
не следует$
сохраняется как есть.- если вы хотите представлять несколько смежных
\
экземпляры, вы должны их; например,:
\
->\
- то же самое, что и просто\
\\
->\
- в входные данные не должны содержать следующие (редко используемые) символы, которые используются для внутренних целей:
0x1
,0x2
,0x3
.- существует в значительной степени гипотетическая озабоченность тем, что если bash должен ввести новый синтаксис расширения, эта функция не может предотвратить такие расширения - см. ниже решение, которое не использует
eval
.
если вы ищете более строгие решения, что только поддерживает
${name}
расширения - то есть, с обязательное фигурные скобки, пренебрегая$name
ссылки - см. ответ шахты.
здесь улучшенная версия bash-only,
eval
-бесплатное решение от принято отвечать:улучшения:
- поддержка расширения как
${name}
и$name
ссылки на переменные.- поддержка
\
-экранирование ссылок на переменные, которые не должны быть расширены.- в отличие от
eval
-на основании вышеуказанного решения ,
- небазовые расширения игнорируются
- ссылки на искаженные переменные игнорируются (они не нарушают сценарий)
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, 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"
эта страница описывает ответ с awk
awk '{while(match(,"[$]{[^}]*}")) {var=substr(,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
идеальный случай для shtpl. (проект Мой, поэтому он не широко используется и не имеет документации. Но вот решение, которое он предлагает в любом случае. Может быть, вы хотите, чтобы проверить его.)
просто выполнить:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
результат:
the number is 1 the word is dog
получать удовольствие.
вот еще одно чистое решение bash:
- он использует heredoc, так что:
- сложность не увеличивается из-за дополнительного синтаксиса
- шаблон может включать в себя код bash
- это также позволяет вам правильно отступать. Увидеть ниже.
- он не использует eval, поэтому:
- нет проблем с рендерингом трейлинг пустых строк
- нет проблем с кавычки в шаблоне
$ cat code
#!/bin/bash LISTING=$( ls ) cat_template() { echo "cat << EOT" cat "" echo EOT } cat_template template | LISTING="$LISTING" bash
$ cat template
(с конечными новыми линиями и двойными кавычками)<html> <head> </head> <body> <p>"directory listing" <pre> $( echo "$LISTING" | sed 's/^/ /' ) <pre> </p> </body> </html>
выход
<html> <head> </head> <body> <p>"directory listing" <pre> code template <pre> </p> </body> </html>
вот еще одно решение: создайте скрипт bash со всеми переменными и содержимым файла шаблона, этот скрипт будет выглядеть так:
word=dog i=1 cat << EOF the number is ${i} the word is ${word} EOF
если мы добавим этот скрипт в bash он будет производить желаемый результат:
the number is 1 the word is dog
вот как создать этот скрипт и передать этот скрипт в bash:
( # Variables echo word=dog echo i=1 # add the template echo "cat << EOF" cat template.txt echo EOF ) | bash
Обсуждение
- скобки открывают вложенную оболочку, ее цель-сгруппировать все выходные данные генерируется
- в оболочке sub мы генерируем все объявления переменных
- также в оболочке sub мы генерируем
cat
команда с HEREDOC- наконец, мы подаем выход sub shell в bash и производим желаемый выход
если вы хотите перенаправить этот вывод в файл, замените последнюю строку с:
) | bash > output.txt
вы также можете использовать bashible (который внутренне использует подход оценки, описанный выше / ниже).
есть пример, как сгенерировать HTML из нескольких частей:
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 < "" }
это чистая функция bash регулируемая по своему вкусу, используемая в производстве и не должна ломаться на любом входе. Если он сломается - дайте мне знать.
вот функция bash, которая сохраняет пробелы:
# Render a file in bash, i.e. expand environment variables. Preserves whitespace. function render_file () { while IFS='' read line; do eval echo \""${line}"\" done < "" }
вот модифицированный
perl
скрипт, основанный на нескольких других ответах:perl -pe 's/([^\]|^)$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/.$ENV{}/eg' -i template
особенности (на основе моих потребностей, но должно быть легко изменить):
- пропускает экранированные расширения параметров (например, \${VAR}).
- поддерживает расширения параметров вида ${VAR}, но не $VAR.
- заменяет ${VAR} пустой строкой, если нет VAR envar.
- поддерживает только символы a-z, A-Z, 0-9 и подчеркивания в имени (исключая цифры в первой позиции).
вместо того чтобы изобретать колесо идти с envsubst Может использоваться практически в любых сценариях, например при создании конфигурационных файлов из переменных среды в контейнерах docker.
если на Mac убедитесь, что у вас есть доморощенного затем свяжите его с gettext:
brew install gettext brew link --force gettext
./шаблон.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
теперь просто использовать это:
# 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