Bash Templating: Как создать файлы конфигурации из шаблонов с помощью Bash?

Я пишу script для автоматизации создания файлов конфигурации для Apache и PHP для своего собственного веб-сервера. Я не хочу использовать графические интерфейсы, такие как CPanel или ISPConfig.

У меня есть несколько шаблонов файлов конфигурации Apache и PHP. Bash script необходимо прочитать шаблоны, сделать замену переменных и выводить анализируемые шаблоны в какую-то папку. Каков наилучший способ сделать это? Я могу думать о нескольких путях. Какой из них лучший, или, может быть, есть некоторые способы сделать это? Я хочу сделать это в чистом Bash (например, это просто в PHP)

1) Как заменить ${} заполнители в текстовом файле?

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"

Кстати, как мне перенаправить вывод на внешний файл здесь? Нужно ли мне что-то избегать, если переменные содержат, скажем, кавычки?

2) Использование cat и sed для замены каждой переменной ее значением:

Данный файл template.txt:

the number is ${i}
the word is ${word}

Команда:

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

Мне кажется плохой из-за необходимости избегать множества разных символов и со многими переменными, длина линии будет длиннее.

Можете ли вы подумать о другом элегантном и безопасном решении?

Ответ 1

Вы можете использовать это:

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

заменить все строки ${...} соответствующими переменными среды (не забудьте экспортировать их перед запуском этого script).

Для чистого 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}"

ПРЕДУПРЕЖДЕНИЕ. Я не знаю, как правильно обрабатывать ввод с помощью NUL в bash или сохранять количество завершающих строк новой строки. Последний вариант представлен как есть, потому что оболочка "любит" двоичный вход:

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

Ответ 2

Попробуйте envsubst

FOO=foo
BAR=bar
export FOO BAR

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

Ответ 3

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

Ответ 4

Я согласен с использованием sed: это лучший инструмент для поиска/замены. Вот мой подход:

$ cat template.txt
the number is ${i}
the dog 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 name is Fido

Ответ 5

Я думаю, что 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 продиктовало решение, которое позволяет избежать проблемы с экранированием bash:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

Изменить: Убрана часть запуска этого root с помощью sudo...

Изменить: Добавлен комментарий о том, как кавычки должны быть экранированы, добавлено решение plockc в микс!

Ответ 6

У меня есть решение bash, такое как mogsie, но с heredoc вместо herestring, чтобы вы не избежали двойных кавычек

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

Ответ 7

Редактировать 6 января 2017 года

Мне нужно было сохранить двойные кавычки в моем файле конфигурации, поэтому двойное экранирование двойных кавычек с помощью sed помогает:

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

Я не могу думать о сохранении новых строк, но между ними сохраняются пустые строки.


Хотя это старая тема, 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 $1)\""
}

user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file

Все кредиты Grégory Pakosz.

Ответ 8

Более длинная, но более надежная версия принятого ответа:

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

Это расширяет все экземпляры $VAR или ${VAR} к их значениям среды (или, если они undefined, пустая строка).

Он должным образом избегает обратных косых черт и принимает обратную косую черту $, чтобы запретить подстановку (в отличие от envsubst, которая, оказывается, не делает этого).

Итак, если ваша среда:

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

и ваш шаблон:

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

результатом будет:

Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it 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($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt

Ответ 9

Я бы сделал это таким образом, возможно, менее эффективным, но легче читать/поддерживать.

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

Ответ 10

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

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

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

Ответ 11

Вместо того, чтобы изобретать велосипед, используйте envsubst. Может использоваться практически в любом сценарии, например, для создания файлов конфигурации из переменных среды в контейнерах докера.

Если на Mac убедитесь, что у вас есть homebrew, тогда свяжите его с 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

Теперь просто используйте это:

# 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

Ответ 12

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

Ответ 13

Если использовать Perl - это вариант, и вы довольствуетесь базовыми расширениями только переменных окружения (в отличие от всех переменных оболочки), считать Стюарт П. Бентли надежный ответ.

Этот ответ призван обеспечить 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 '`([' '\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' '`(['
}

<сильные > Примеры:

$ 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)}
    • Такие внедренные подстановки фактически BREAK функции (потому что все экземпляры $( и ` слепо экранированы).
    • Аналогично, неправильные ссылки на переменные, такие как ${HOME (отсутствует закрытие }) BREAK функция.
  • Из-за bash обработки строк с двойными кавычками обратные косые черты обрабатываются следующим образом:
    • \$name предотвращает расширение.
    • Единственный \, за которым не следует $, сохраняется как есть.
    • Если вы хотите представлять несколько соседних экземпляров \, вы должны их удвоить; например.:
      • \\\ - то же самое, что только \
      • \\\\\\
    • Ввод не должен содержать следующие (редко используемые) символы, которые используются для внутренних целей: 0x1, 0x2, 0x3.
  • В значительной степени гипотетическая проблема заключается в том, что если bash должен ввести новый синтаксис расширения, эта функция может не помешать таким расширениям - см. ниже решение, которое не использует eval.

Если вы ищете более ограничительное решение, которое поддерживает только расширения ${name} - то есть с обязательными фигурными фигурными скобками, игнорируя ссылки $name - см. мой ответ.


Вот улучшенная версия bash -одно, eval -бесплатное решение из принятого ответа:

Улучшения:

  • Поддержка расширения ссылок ${name} и $name.
  • Поддержка \ -escaping ссылок переменных, которые не следует расширять.
  • В отличие от вышеприведенного решения eval
    • неосновные расширения игнорируются
    • неверные ссылки на ссылки игнорируются (они не нарушают 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 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 value using indirect expansion
           lines=${pre}${!varName}${post}
      fi
      end_offset=${#pre}
 done
 printf %s "$lines"

Ответ 14

Здесь другое чистое решение bash:

  • используя heredoc, так:
    • сложность не увеличивается из-за дополнительного требуемого синтаксиса
    • может содержать bash код
      • который также позволяет вам делать отступы. См. Ниже.
  • он не использует eval, поэтому:
    • нет проблем с отображением завершающих пустых строк
    • проблем с кавычками в шаблоне

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  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>

Ответ 15

На этой странице описывается ответ с awk

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

Ответ 16

Идеальный случай для shtpl. (проект моего, так что он не широко используется и отсутствует в документации. Но вот решение, которое оно предлагает в любом случае. Можете ли вы его протестировать.)

Просто выполните:

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

Результат:

the number is 1
the word is dog

Удачи.

Ответ 17

Вот еще одно решение: сгенерируйте скрипт 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

обсуждение

  • В скобках открывается суб-оболочка, ее цель состоит в объединении всех полученных результатов
  • Внутри подклассов мы генерируем все объявления переменных
  • Также в подклассе мы генерируем команду cat с помощью HEREDOC
  • Наконец, мы подаем выходной сигнал суб-оболочки в bash и получаем желаемый результат
  • Если вы хотите перенаправить этот вывод в файл, замените последнюю строку следующим образом:

    ) | bash > output.txt
    

Ответ 18

Вы также можете использовать bashible (который внутренне использует подход оценки, описанный выше/ниже).

Есть пример, как создать HTML из нескольких частей:

https://github.com/mig1984/bashible/tree/master/examples/templates

Ответ 19

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

Это чистая функция bash, настраиваемая по своему вкусу, используемая в производстве и не должна прерываться при вводе. Если он сломается - сообщите мне.

Ответ 20

Здесь функция 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 < "${1}"
}

Ответ 21

Здесь приведено изменение perl script на основе нескольких других ответов:

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

Возможности (основанные на моих потребностях, но их легко изменить):

  • Пропускает экранированные расширения параметров (например,\${VAR}).
  • Поддерживает разложение параметров формы ${VAR}, но не $VAR.
  • Заменяет ${VAR} пустой строкой, если нет VAR-envar.
  • Поддерживает только символы a-z, A-Z, 0-9 и подчеркивания в имени (исключая цифры в первой позиции).

Ответ 22

Посмотрите на скрипт Python для подстановки простых переменных здесь: https://github.com/jeckep/vsubst

Это очень просто использовать:

python subst.py --props secure.properties --src_path ./templates --dst_path ./dist