我正在编写一个脚本来自动为我自己的网络服务器创建 Apache 和 PHP 的配置文件。我不想使用任何图形用户界面,如 CPanel 或 ISPConfig。
我有一些 Apache 和 PHP 配置文件的模板。 Bash 脚本需要读取模板,进行变量替换并将解析的模板输出到某个文件夹中。最好的方法是什么?我可以想到几种方法。哪一个是最好的,或者可能有更好的方法来做到这一点?我想在纯 Bash 中做到这一点(例如在 PHP 中很容易)
如何替换文本文件中的 ${} 占位符?
模板.txt:
The number is ${i}
The word is ${word}
脚本.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"
顺便说一句,我如何在这里将输出重定向到外部文件?如果变量包含引号,我是否需要转义?
使用 cat & sed 将每个变量替换为其值:
给定 template.txt(见上文)
命令:
cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"
对我来说似乎很糟糕,因为需要转义许多不同的符号并且有很多变量,这条线太长了。
你能想到其他一些优雅和安全的解决方案吗?
试试 envbust
试试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).
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
关于 yottsa's answer:envsubst
对我来说是新的。极好的。
envsubst
更喜欢这个,因为它从我的 Dockerfile 中的额外 apt-get install gettext-base
中保存了我
你可以使用这个:
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 “喜欢”二进制输入:
read 将解释反斜杠。 read -r 不会解释反斜杠,但如果最后一行不以换行符结尾,它仍然会删除。 "$(...)" 将删除尽可能多的尾随换行符,所以我以 ; 结尾... echo -na 并使用 echo -n "${line:0:-1}":这会删除最后一个字符(即 a)并保留与输入中相同的尾随换行符(包括 no)。
[^}]
更改为 [A-Za-Z_][A-Za-z0-9_]
以防止 shell 超出严格替换(例如,如果它试图处理 ${some_unused_var-$(rm -rf $HOME)}
)。
$&
更改为 ""
:如果替换失败,首先保留 ${...}
不变,然后用空字符串替换它。
while
循环读取最后一行,即使它没有被换行符终止,请使用 while read -r line || [[ -n $line ]]; do
。此外,您的 read
命令从每行中去除前导和尾随空格;为避免这种情况,请使用 while IFS= read -r line || [[ -n $line ]]; do
\
- 转义它们)。
我同意使用 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
-i
选项(就地编辑文件)。检查您的 sed 的手册页。
我有一个像 mogsie 这样的 bash 解决方案,但使用 heredoc 而不是 herestring 可以避免转义双引号
eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
${param:?}
的 必需参数 和嵌套文本 around 可选参数。示例:当 DELAY 未定义且 <delay>17</delay> 时,${DELAY:+<delay>$DELAY</delay>}
扩展为空。当延迟 = 17。
_EOF_$$
。
$trailing_newline
,或使用 $NL5
并确保它被扩展为 5 个换行符。
template.txt
将起作用,以保留尾随换行符。
eval
,如果 template.txt
在其自身的一行中包含 EOF
,它将过早终止 here-doc 并因此中断命令。 (向@xebeche 致敬)。
尝试评估
我认为 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 的解决方案!
eval
ed echo
/ cat
命令时的字符串分隔符处理它们;试试 eval "echo \"'\$HOME'\""
。
编辑 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 的所有功劳。
eval "echo \"$(sed 's/\"/\\"/g' $1)\""
$variables
)。
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
envsubst
的这个调用序列确实有效。
envsubst
在 MacOS 上不起作用,您需要使用自制软件安装它:brew install gettext
。
我会这样做,可能效率较低,但更容易阅读/维护。
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
sed -e 's/VARONE/NEWVALA/g' -e 's/VARTWO/NEWVALB/g' -e 's/VARTHR/NEWVALC/g' < $TEMPLATE > $OUTPUT
已接受答案的更长但更强大的版本:
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
这是另一个解决方案:使用模板文件的所有变量和内容生成一个 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命令
最后,我们将 sub shell 输出提供给 bash 并产生所需的输出
如果您想将此输出重定向到文件中,请将最后一行替换为:) | bash > 输出.txt
这是另一个纯 bash 解决方案:
它使用heredoc,因此:复杂性不会增加,因为额外需要的语法模板可以包含bash代码,它还允许您正确缩进内容。见下文。
由于额外需要语法,复杂性不会增加
模板可以包含 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>
使用纯 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
如果使用 Perl 是一种选择,并且您满足于仅基于 环境 变量的扩展(而不是所有 shell em> 个变量),考虑 Stuart P. Bentley's robust answer。
此答案旨在提供一个仅限 bash 的解决方案,尽管使用了 eval
,但它应该可以安全使用。
目标是:
支持 ${name} 和 $name 变量引用的扩展。
防止所有其他扩展:命令替换($(...) 和传统语法 `...`)算术替换($((...)) 和传统语法 $[...])。
命令替换($(...) 和传统语法 `...`)
算术替换 ($((...)) 和旧语法 $[...])。
允许通过前缀 \ (\${name}) 选择性地抑制变量扩展。
保留特殊字符。在输入中,特别是 " 和 \ 实例。
允许通过参数或标准输入进行输入。
函数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
出于性能原因,该函数将标准输入输入一次全部读入内存,但很容易将该函数调整为逐行方法。
还支持非基本变量扩展,例如 ${HOME:0:10},只要它们不包含嵌入式命令或算术替换,例如 ${HOME:0:$(echo 10)} 这样的嵌入式替换实际上 BREAK函数(因为所有 $( 和 ` 实例都被盲目转义了)。类似地,格式错误的变量引用,例如 ${HOME (missing closing }) BREAK 该函数。
这种嵌入式替换实际上破坏了函数(因为所有 $( 和 ` 实例都被盲目地转义了)。
同样,格式错误的变量引用,例如 ${HOME (missing closing }) BREAK 该函数。
由于 bash 对双引号字符串的处理,反斜杠的处理方式如下: \$name 防止扩展。不跟 $ 的单个 \ 将按原样保留。如果要表示多个相邻的 \ 实例,则必须将它们加倍;例如:\\ -> \ - 与 \ \\\\ -> \\ 相同 输入不得包含以下(很少使用)字符,这些字符用于内部用途:0x1、0x2、0x3。
\$name 防止扩展。
不跟 $ 的单个 \ 将按原样保留。
如果要表示多个相邻的 \ 实例,则必须将它们加倍;例如: \\ -> \ - 与 \ \\\\ -> \\ 相同
\\ -> \ - 与 \ 相同
\\\\ -> \\
输入不得包含以下(很少使用)字符,这些字符用于内部用途:0x1、0x2、0x3。
有一个很大的假设是,如果 bash 应该引入新的扩展语法,这个函数可能不会阻止这种扩展 - 请参阅下面的不使用 eval 的解决方案。
如果您正在寻找仅支持${name}
扩展的更具限制性的解决方案 - 即,使用 强制花括号,忽略 $name
参考 - 见我的this answer。
以下是来自 accepted answer 的仅 bash、无 eval
解决方案的改进版本:
改进如下:
支持 ${name} 和 $name 变量引用的扩展。
支持 \-escaping 不应扩展的变量引用。
与上述基于 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"
试试 shtpl
shtpl 的完美案例。 (我的项目,所以它没有被广泛使用并且缺乏文档。但无论如何这是它提供的解决方案。你想测试它。)
只需执行:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
结果是:
the number is 1
the word is dog
玩得开心。
本页描述了一个 answer with awk
awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
# 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 功能,可根据您的喜好进行调整,用于生产,不应因任何输入而中断。如果它坏了 - 让我知道。
要跟进此页面上的 plockc's answer,这里有一个适合破折号的版本,适合那些希望避免 bashism 的人。
eval "cat <<EOF >outputfile
$( cat template.in )
EOF
" 2> /dev/null
您还可以使用 bashible(它在内部使用上面/下面描述的评估方法)。
有一个例子,如何从多个部分生成 HTML:
https://github.com/mig1984/bashible/tree/master/examples/templates
在这里查看简单的变量替换 python 脚本:https://github.com/jeckep/vsubst
使用非常简单:
python subst.py --props secure.properties --src_path ./templates --dst_path ./dist
这是一个保留空格的 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} 替换为空白字符串。
仅支持名称中的 az、AZ、0-9 和下划线字符(不包括第一位的数字)。
您还可以使用 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
不定期副业成功案例分享
envsubst
,因为 bash 将 heredoc 视为文字双引号字符串并已在其中插入变量。但是,当您想从另一个文件中读取模板时,这是一个不错的选择。可以很好地替代更麻烦的m4
。envsubst
是一个 GNU gettext 实用程序,实际上并不是那么健壮(因为 gettext 用于本地化人类消息)。最重要的是,它不识别反斜杠转义的 ${VAR} 替换(因此您不能拥有在运行时使用 $VAR 替换的模板,例如 shell 脚本或 Nginx conf 文件)。有关处理反斜杠转义的解决方案,请参阅 my answer。<<"EOF"
,它 不 插入变量(带引号的终止符就像单heredocs 的引用)。cat template.txt | envsubst