Bash Templating: как создавать файлы конфигурации из шаблонов с помощью Bash?
Я пишу сценарий для автоматизации создания файлов конфигурации для Apache и PHP для моего собственного веб-сервера. Я не хочу использовать какие-либо GUIs, такие как CPanel или ISPConfig.
у меня есть несколько шаблонов файлов конфигурации Apache и PHP. Скрипт Bash должен читать шаблоны, делать подстановку переменных и выводить разбираемые шаблоны в какую-то папку. Как лучше всего это сделать? Я могу придумать несколько способов. Какой из них лучший или, может быть, есть несколько лучших способов сделать это? Я хочу сделать это в pure 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
заменить все ${...}
строки с соответствующими переменными среды (не забудьте экспортировать их перед запуском этого скрипта).
для pure bash это должно работать (предполагая, что переменные не содержат ${...} 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
. Решение, которое не зависает, если 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 работает очень хорошо. Он обрабатывает шаблоны с linebreaks, пробелами и всевозможными вещами 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 quote escaping:
$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Edit: удалена часть о запуске этого как root с помощью sudo...
Edit: добавлен комментарий о том, как кавычки должны быть экранированы, добавлено решение plockc в микс!
Редактировать 6 Января 2017
мне нужно было сохранить двойные кавычки в файле конфигурации, поэтому двойное экранирование двойных кавычек с помощью sed помогает:
render_template() {
eval "echo \"$(sed 's/\"/\\"/g' )\""
}
Я не могу думать о сохранении новых линий, но пустые строки между ними сохраняются.
хотя это старая тема, IMO я нашел более элегантное решение здесь: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}
к их значениям среды (или, если они не определены, пустой строке).
он правильно избегает обратных косых черт и принимает обратную косую черту-экранированный$, чтобы заблокировать замену (в отличие от 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, но с новым соответствием regex стиля и косвенной заменой параметров, он становится:
#!/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 input все сразу в память, но легко приспособить функцию к построчному подходу.
- также поддерживает небазовые переменные расширения, такие как
${HOME:0:10}
, если они не содержат встроенных команд или арифметических подстановок, таких как${HOME:0:$(echo 10)}
- такие встроенные замены фактически нарушают функцию (потому что все
$(
и`
экземпляры слепо бежал). - аналогично, искаженные ссылки на переменные, такие как
${HOME
(отсутствует закрывающий тег}
) сломать функцию.
- такие встроенные замены фактически нарушают функцию (потому что все
- из-за обработки bash строки с двойными кавычками, обратные косые черты обрабатываются следующим образом:
-
$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 shell мы генерируем
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