Bash Templating:如何使用Bash从模板构建配置文件?

时间:2010-05-26 15:12:17

标签: bash templates templating

我正在编写一个脚本来自动为我自己的网络服务器创建Apache和PHP的配置文件。我不想使用任何GUI,如CPanel或ISPConfig。

我有一些Apache和PHP配置文件的模板。 Bash脚本需要读取模板,进行变量替换并将解析后的模板输出到某个文件夹中。最好的方法是什么?我可以想到几种方法。哪一个是最好的还是有更好的方法可以做到这一点?我想在纯Bash中做到这一点(例如在PHP中很容易)

1)How to replace ${} placeholders in a text file?

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&amp; sed用其值替换每个变量:

给出template.txt:

The number is ${i}
The word is ${word}

命令:

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

对我来说似乎不好,因为需要逃避许多不同的符号,而且许多变量的行太长了。

你能想到其他一些优雅而安全的解决方案吗?

24 个答案:

答案 0 :(得分:108)

尝试envsubst

FOO=foo
BAR=bar
export FOO BAR

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

答案 1 :(得分:56)

您可以使用:

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/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中正确处理NUL的输入或保留尾随换行的数量。最后一个变体是因为它是“喜欢”二进制输入:

  1. read将解释反斜杠。
  2. read -r不会解释反斜杠,但如果它不以换行符结尾,仍将删除最后一行。
  3. "$(…)"会删除与现有数量相同的跟踪换行符,因此我使用结束; echo -n a并使用echo -n "${line:0:-1}":这会删除最后一个字符(即{ {1}})并保留与输入中一样多的尾随换行符(包括否)。

答案 2 :(得分:36)

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

答案 3 :(得分:31)

我同意使用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

答案 4 :(得分:18)

我认为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吃掉。

如果您更喜欢catecho

,也可以在此使用此处的文档
$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null

@plockc提出了一个避免bash引用转义问题的解决方案:

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

修改:删除了有关使用sudo以root身份运行此内容的部分...

修改:添加了关于如何转义报价的评论,添加了plockc的解决方案!

答案 5 :(得分:16)

我有像mogsie这样的bash解决方案但是使用heredoc而不是herestring来避免转义双引号

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

答案 6 :(得分:16)

2017年1月6日编辑

我需要在配置文件中保留双引号,以便使用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的所有积分。

答案 7 :(得分:9)

接受答案的更长但更强大的版本:

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}的所有实例扩展到其环境值(或者,如果它们未定义,则为空字符串)

它正确地逃避反斜杠,并接受反斜杠转义$来禁止替换(与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($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt

答案 8 :(得分:8)

我这样做了,效率可能不高,但更容易阅读/维护。

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

答案 9 :(得分:7)

如果您想使用Jinja2模板,请参阅此项目:j2cli

它支持:

  • 来自JSON,INI,YAML文件和输入流的模板
  • 模仿环境变量

答案 10 :(得分:6)

envsubst 代替了重新发明轮子 几乎可以在任何情况下使用,例如从docker容器中的环境变量构建配置文件。

如果在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

答案 11 :(得分:5)

使用纯粹的bash从ZyX获得答案,但是使用新的样式正则表达式匹配和间接参数替换,它变为:

#!/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

答案 12 :(得分:4)

如果使用 Perl 是一个选项,并且您满足于仅在 环境变量上进行扩展(而不是所有 shell 变量),考虑Stuart P. Bentley's robust answer

此答案旨在提供仅限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阻止扩展。
    • 单个\未跟$原样保留。
    • 如果您想表示多个相邻的 \个实例,则必须将它们加倍;例如。:
      • \\ - &gt; \ - 与\
      • 相同
      • \\\\ - &gt; \\
    • 输入不得包含以下(很少使用)字符,这些字符用于内部目的:0x10x20x3
  • 如果bash应该引入新的扩展语法,那么这个函数可能无法阻止这种扩展,这是一个很大程度上假设的问题 - 请参阅下面的解决方案,不使用eval

如果您正在寻找更具限制性的解决方案支持${name}扩展 - 即使用强制性花括号,忽略$name引用 - 请参阅我的this answer


以下是来自accepted answer 的仅限bash的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"

答案 13 :(得分:4)

这是另一个纯粹的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>

答案 14 :(得分:3)

shtpl的完美案例。 (我的项目,因此它没有广泛使用,缺乏文档。但无论如何,这里提供的解决方案。你可以测试一下吗。)

执行:

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

结果是:

the number is 1
the word is dog

玩得开心。

答案 15 :(得分:3)

这是另一个解决方案:生成一个包含所有变量和模板文件内容的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

讨论

  • 括号打开一个子shell,其目的是将所有生成的输出组合在一起
  • 在子shell中,我们生成所有变量声明
  • 同样在子shell中,我们使用HEREDOC生成cat命令
  • 最后,我们将子shell输出提供给bash并生成所需的输出
  • 如果要将此输出重定向到文件中,请将最后一行替换为:

    ) | bash > output.txt
    

答案 16 :(得分:3)

此页面介绍了answer with awk

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

答案 17 :(得分:1)

您还可以使用 bashible (内部使用上述/下文所述的评估方法)。

有一个例子,如何从多个部分生成HTML:

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

答案 18 :(得分:1)

# 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功能,可以根据自己的喜好调整,在生产中使用,不应该在任何输入上中断。 如果它坏了 - 请告诉我。

答案 19 :(得分:0)

这是一个保留空白的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}"
}

答案 20 :(得分:0)

以下是基于其他一些答案的修改后的perl脚本:

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

功能(根据我的需要,但应该很容易修改):

  • 跳过转义参数扩展(例如\ $ {VAR})。
  • 支持$ {VAR}形式的参数扩展,但不支持$ VAR。
  • 如果没有VAR envar,则用空字符串替换$ {VAR}。
  • 仅支持名称中的a-z,A-Z,0-9和下划线字符(第一个位置的数字除外)。

答案 21 :(得分:0)

在这里查看简单的变量替换python脚本:https://github.com/jeckep/vsubst

使用非常简单:

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

答案 22 :(得分:0)

我对这个奇妙问题的谦虚贡献。

tpl() {
    local file=$(cat - | \
                 sed -e 's/\"/\\"/g' \
                     -e "s/'/\\'/g")
    local vars=$(echo ${@} | tr ' ' ';')
    echo "$(sh -c "${vars};echo \"$file\"")"
}

cat tpl.txt | tpl "one=fish" "two=fish"

这实际上是通过使用subshel​​l进行envar替换而起作用的,除了它不使用eval并且显式转义了单引号和双引号。它将var表达式连接成一行,没有空格,不会混淆sh,然后将模板传递给echo,让sh处理var替换。它保留换行符,注释等...,如果您不希望对var进行解释,则可以转义\${like_this}${missing_var}将被替换为空值。

这里的许多其他答案都非常好,但是我想要的是非常简单的东西,它不需要为我当前的模板案例处理所有可能的案例。

享受!

答案 23 :(得分:0)

要继续关注此页面上的plockc's answer,以下是适用于仪表板的版本,适合那些希望避免使用宗教信仰的人。

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