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

我正在编写一个脚本来自动为我自己的 web 服务器创建 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"

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

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

给定 template.txt (见上文)

命令:

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

对我来说似乎很糟糕,因为需要转义很多不同的符号,而且有很多变量,这条线会太长。

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

169253 次浏览

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

你可以用这个:

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 中使用 NULs 的输入,也不知道如何保留后续换行符的数量。最后一个变体的出现是因为 shell“喜欢”二进制输入:

  1. read将解释反斜杠。
  2. read -r不会解释反斜杠,但是如果最后一行没有以换行符结束,它仍然会删除该行。
  3. "$(…)"会去掉所有现有的拖尾换行符,所以我用 ; echo -n a结束 ,并使用 echo -n "${line:0:-1}": 这会去掉最后一个字符(即 a) ,并保留输入中所有的拖尾换行符(包括 no)。

我会这样做,可能效率较低,但更容易阅读/维护。

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

本页描述 用警告回答

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

试试 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).

Herdoc是模板 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

关于 Yottsa 的回答: envsubst对我来说是新的。太棒了。

试试 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..。

编辑: 添加了关于如何转义引号的注释,将 plockc 的解决方案添加到混合!

试试看

真是的的完美案例。由于我的一个项目,所以使用范围不广,缺乏文献资料。但无论如何,这是它提供的解决方案。也许你想测试一下。)

只要执行:

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

结果是:

the number is 1
the word is dog

玩得开心。

我有一个 bash 的解决方案,像 mogsie,但与 herdoc 而不是 herstring,让你避免逃避双引号

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

Edit Jan 6,2017编辑: 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

所有学分到 Gregory Pakosz

如果要使用 Jinja2模板,请参见此项目: J2cli

它支持:

  • 来自 JSON、 INI、 YAML 文件和输入流的模板
  • 从环境变量进行模板化

一个更长但更有说服力的公认答案:

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 < em > or ${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

从 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变量相反) ,则使用 考虑一下斯图尔特 · 本特利(Stuart P. Bentley)的强有力的回答

这个答案旨在提供一个 只使用 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)}
    • 这种嵌入式替换实际上破坏了函数(因为所有 $(`实例都是盲目转义的)。
    • 类似地,格式不正确的变量引用,如 ${HOME(缺少关闭 })破坏函数。
  • 由于 bash 对双引号字符串的处理,反斜杠的处理方式如下:
    • \$name防止膨胀。
    • 单个 \后面没有跟随 $保留原样。
    • 如果要表示 多重相邻 \实例,则必须使用 加倍; 例如:
      • \\-> \-和 \一样
      • - > \\
    • 输入不能包含下列(很少使用的)字符,这些字符用于内部目的: 0x10x20x3
  • 如果 bash 应该引入新的扩展语法,那么这个函数可能不会阻止这样的扩展——请参阅下面不使用 eval的解决方案。

如果你正在寻找一个 更具限制性的解决方案,即 < em > 仅 支持 ${name}扩展-即,与 强制性的花括号,忽略 $name引用-参见我的 这个答案


这是一个 来自 < a href = “ https://stackoverflow. com/a/2916159/45375”> 接受答案 的纯 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"

您还可以使用 卑鄙无耻(它在内部使用上/下面描述的评估方法)。

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

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

下面是一个保留空格的 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}"
}

下面是一个基于其他几个答案的修改过的 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和下划线字符(不包括第一个位置的数字)。

下面是另一个纯 bash 解决方案:

  • 它使用的是 herdoc,所以:
    • 由于额外需要的语法,复杂性不会增加
    • 模板可以包括 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>
# 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 函数,在生产中使用,不应在任何输入上中断。 如果坏了,告诉我。

下面是另一个解决方案: 生成一个 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
    

而不是重造轮子 < strong > < a href = “ https://www.gnu.org/software/gettext/hand/html _ node/envsubst-Invocation.html”rel = “ norefrer”> envsubst 几乎可以在任何场景中使用,例如从 docker 容器中的环境变量构建配置文件。

如果在 mac 上,确保你有 自酿的,然后链接到 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

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

它的使用非常简单:

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

为了跟进本页的 Plockc 的回答,这里有一个适合破折号的版本,可以让你避免中伤。

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

您还可以使用 printf 来填充模板。

#!/bin/bash


IFS='' read -rd '' TEMPL <<-'EOB'
The number is %d
The word is "%s"
Birds of Massachusetts:
%s








EOB


N=12
WORD="Bird"
MULTILINE="Eastern Bluebirds
Common Grackles"


echo "START"
printf "${TEMPL}" ${N} ${WORD} "${MULTILINE}"
echo "END"

下面是输出,引号和空格完好无损:

START
The number is 12
The word is "Bird"
Birds of Massachusetts:
Eastern Bluebirds
Common Grackles








END