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 89

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 или сохранять количество завершающих новых строк. Последний вариант представлен так, как он есть, потому что оболочки "любят" двоичный вход:

  1. read будет интерпретировать обратные косые черты.
  2. read -r не будет интерпретировать обратные косые черты, но все равно отбросит последнюю строку, если она не заканчивается новой строкой.
  3. "$(…)" будет лишать столько конечных новых строк, сколько есть, поэтому я заканчиваю С ; echo -n a и использовать echo -n "${line:0:-1}": это отбрасывает последний символ (который является a) и сохраняет столько конечных новых строк, сколько было во входных данных (включая no).

попробовать envsubst

FOO=foo
BAR=bar
export FOO BAR

envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF

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

Если вы хотите использовать Jinja2 шаблоны, смотрите этот проект:j2cli.

Он поддерживает:

  • шаблоны из JSON, INI, YAML файлов и входных потоков
  • шаблоны из переменных окружения

принимая ответ от 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