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

188

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

我有一些Apache和PHP配置文件的模板。Bash脚本需要读取模板,进行变量替换,并将解析后的模板输出到某个文件夹中。最好的方法是什么?我能想到几种方法。哪种是最好的,或者可能有更好的方法?我想在纯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"

顺便问一下,在这里如何将输出重定向到外部文件?如果变量包含引号,我需要转义什么吗?

  1. 使用cat和sed替换每个变量为其值:

给定template.txt(见上文)

命令:

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

我认为这样做不太好,因为需要转义很多不同的符号,而且如果有很多变量,那么这一行代码会非常长。

你能想到其他更优雅和安全的解决方案吗?


这个回答解决了你的问题吗?如何替换文本文件中的${}占位符? - Ky -
如果您有 PHP 这样的强大模板语言可用,那么“纯 Bash”要求似乎是不必要的。 - jchook
26个回答

230

尝试使用envsubst

$ cat envsubst-template.txt
Variable FOO is (${FOO}).
Variable BAR is (${BAR}).

$ FOO=myfoo

$ BAR=mybar

$ export FOO BAR

$ cat envsubst-template.txt | envsubst
Variable FOO is (myfoo).
Variable BAR is (mybar).

19
仅供参考,当使用heredoc时,不需要使用envsubst,因为bash会将heredoc视为一个双引号括起来的字符串并对其中的变量进行插值。但如果想要从另一个文件中读取模板,则envsubst是一个很好的选择。它是替代更加繁琐的m4的好方法。 - beporter
3
得知这个命令让我非常惊喜。我一直在尝试手动模拟envsubst的功能,但却毫无进展。感谢yottatsa! - Tim Stewart
5
注意:envsubst 是 GNU gettext 工具之一,实际上并不是很健壮(因为gettext旨在本地化人类消息)。最重要的是,它无法识别反斜杠转义的${VAR}替换(因此您无法在运行时使用$VAR替换的模板,例如shell脚本或Nginx配置文件)。请参见我的答案,了解处理反斜杠转义的解决方案。 - Stuart P. Bentley
4
在这种情况下,如果你出于某种原因想要将此模板传递给envsubst,你需要使用<<"EOF",它不会插值变量(引用终止符类似于heredocs的单引号)。 - Stuart P. Bentley
12
我会尽力进行翻译,以下是您提供的内容:我用它的方式是:cat template.txt | envsubst - daparic
显示剩余3条评论

80

heredoc是一种内置的模板配置文件的方法。

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

关于yottsa的回答:我之前不知道envsubst,太棒了。

3
我更喜欢这个比envsubst,因为它让我在Dockerfile中省去了额外的apt-get install gettext-base - daparic
作为一个类似模板的脚本,它不需要安装任何外部库,也不需要应对棘手的表达式。 - 千木郷
我偏爱的解决方案! - bux

69
你可以使用这个:
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字符的输入或保留尾随换行符的方法。最后一种变体是原样呈现的,因为shell "喜欢"二进制输入:

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

3
我会将bash版本中的 [^}] 改为 [A-Za-Z_][A-Za-z0-9_],以防止shell在进行严格替换时超出范围(例如,如果尝试处理${some_unused_var-$(rm -rf $HOME)})。 - Chris Johnsen
2
@FractalizeR,你可能想把Perl解决方案中的$&改为"":第一个保留${...}如果替换失败,则不变,第二个用空字符串替换它。 - ZyX
5
注意:显然从bash 3.1到3.2(以及更高版本)发生了变化,单引号将正则表达式的内容视为一个字符串字面量。 因此上面的正则表达式应该是...(${[a-zA-Z_][a-zA-Z_0-9]*})https://dev59.com/oHVC5IYBdhLWcg3wZwTj - Blue Waters
2
为了使while循环读取最后一行,即使它没有以换行符结尾,使用while read -r line || [[ -n $line ]]; do。 此外,您的read命令会从每行中剥离前导和尾随空格;为了避免这种情况,使用while IFS= read -r line || [[ -n $line ]]; do - mklement0
2
只是要注意一个限制,对于那些寻求全面解决方案的人:这些本来很方便的解决方案不允许您有选择地保护变量引用免受扩展(例如通过\转义它们)。 - mklement0
显示剩余10条评论

42

我同意使用sed:它是最好的搜索/替换工具。这是我的方法:

$ cat template.txt
the number is ${i}
the dog's name is ${name}

$ cat replace.sed
s/${i}/5/g
s/${name}/Fido/g

$ sed -f replace.sed template.txt > out.txt

$ cat out.txt
the number is 5
the dog's name is Fido

1
这需要用到临时文件来替换字符串,对吗?有没有一种不需要使用临时文件的方法呢? - Vladislav Rastrusny
@FractalizeR:一些版本的sed有一个-i选项(原地编辑文件),类似于perl选项。请查看您的sed的man页面。 - Chris Johnsen
@FractalizeR 是的,sed -i 将会替换内联。如果你熟悉 Tcl(另一种脚本语言),那么可以查看这个线程:https://dev59.com/pE3Sa4cB1Zd3GeqPxr2r#2837289 - Hai Vu
我使用以下sed命令从属性文件创建了replace.sed:sed -e 's/^/s/${/g' -e 's/=/}//g' -e 's/$///g' the.properties > replace.sed。 - Jaap D
1
@hai vu的代码创建了一个sed程序,并使用sed的-f标志将该程序传递进去。如果您愿意,也可以使用-e标志将sed程序的每一行传递给sed。就我而言,我喜欢使用sed进行模板化的想法。 - Andrew Allbright

37

我有一个类似于mogsie的Bash解决方案,但是使用heredoc而不是herestring,这样可以避免转义双引号。

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

7
这个解决方案支持在模板中使用Bash参数扩展。 我喜欢的是${param:?}表示_必需参数_,以及在可选参数_周围_嵌套文本。例如:${DELAY:+<delay>$DELAY</delay>} 当DELAY未定义时会扩展为空,当DELAY=17时会扩展为<delay>17</delay> - Eric Bolinger
3
哦!而且EOF分隔符可以使用动态字符串,例如PID _EOF_$$ - Eric Bolinger
1
@mklement0 针对行末有换行符的解决方法是使用扩展技巧,例如一个空变量$trailing_newline,或者使用$NL5并确保它被扩展为5个换行符。 - xebeche
1
一种优雅的解决方案,但请注意命令替换将从输入文件中剥离任何尾随换行符,尽管通常不会出现问题。 另一个边缘情况:由于使用了 eval,如果 template.txt 包含单独一行的 EOF,它将过早地终止 here-doc,从而破坏命令。(向 @xebeche 致敬)。 - mklement0
1
回答我的问题:是的,这与执行eval "$(<template.txt)" 2> /dev/null不同,因为如果使用cat,则将打印模板,但在我上面的情况下,eval将评估模板字符串并基本上尝试将其作为代码运行。 - smac89
显示剩余3条评论

27

尝试使用eval

我认为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

编辑:已删除有关使用sudo以root身份运行此操作的部分...

编辑:添加了有关需要转义引号的注释,并将plockc的解决方案加入其中!


这会删除您模板中的引号,并且不会替换单引号内部的内容,因此根据您的模板格式,可能会导致微妙的错误。虽然这可能适用于任何基于Bash的模板方法。 - Alex B
在我看来,基于Bash的模板是一种疯狂的做法,因为你需要成为一个Bash程序员才能理解你的模板在做什么!但还是感谢您的评论! - mogsie
@AlexB:这种方法将会在单引号之间进行替换,因为它们只是双引号字符串中的字面字符,而不是eval执行的echo/cat命令处理它们时的字符串分隔符;尝试使用eval "echo \"'\$HOME'\"". - mklement0

18

编辑于2017年1月6日

我需要在我的配置文件中保留双引号,所以使用sed双转义双引号可以帮助解决问题:


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

我无法考虑保留多余的空行,但是段落之间的空行会被保留。


虽然这已经是一个老话题了,在我看来,我在这里找到了更优美的解决方案: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


这将从输入中删除双引号,并且如果输入文件中有多个尾随换行符,则将它们替换为单个换行符。 - mklement0
3
为使其工作,我需要少两个反斜杠,即 eval "echo \"$(sed 's/\"/\\"/g' $1)\""。该命令用于替换文本中的双引号并将结果传递给 echo 命令进行输出。 - David Bau
不幸的是,这种方法不允许您模板化包含$variables的php文件。 - IStranger

17

不要重复造轮子,请使用envsubst。它几乎可以在任何场景中使用,例如从Docker容器的环境变量构建配置文件。

如果在Mac上,请确保您已经安装了Homebrew,然后将其链接到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 depends on tools that utilise .env file 
# automatically like pipenv etc. 
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh

这个 envsubst 的调用序列实际上是有效的。 - Alexander Oh
1
对于其他人来说,envsubst 在 MacOS 上不起作用,你需要使用 homebrew 安装它:brew install gettext - Matt
我喜欢使用cat template.cfg | envsubst > whatever.cfg这种方法,简洁明了。做得不错。 - undefined

13

我会这样做,可能不太高效,但更易于阅读/维护。

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

16
你可以使用一条 sed 命令来完成此操作,而不必逐行阅读。命令为:sed -e 's/VARONE/NEWVALA/g' -e 's/VARTWO/NEWVALB/g' -e 's/VARTHR/NEWVALC/g' < $TEMPLATE > $OUTPUT。该命令将会替换文件 $TEMPLATE 中的所有 VARONENEWVALA,所有的 VARTWO 替换为 NEWVALB,以及所有的 VARTHR 替换为 NEWVALC,并将结果输出到 $OUTPUT 文件中。 - Brandon Bloom

12
如果您想使用Jinja2模板,请参考此项目:j2cli
它支持:
- 从JSON、INI、YAML文件和输入流中获取模板 - 从环境变量进行模板化

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接