Bash模板:如何使用Bash从模板构buildconfiguration文件?
我正在编写一个脚本来为我自己的web服务器自动创buildApache和PHP的configuration文件。 我不想使用任何GUI,如CPanel或ISPConfig。
我有一些Apache和PHPconfiguration文件的模板。 Bash脚本需要读取模板,进行variablesreplace并将parsing后的模板输出到某个文件夹中。 什么是最好的方法来做到这一点? 我可以想到几种方法。 哪一个是最好的,或者可能有一些更好的方法来做到这一点? 我想在纯粹的Bash中做到这一点(在PHP中很容易)
1) 如何在文本文件中replace$ {}占位符?
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"
顺便说一句,我如何redirect输出到外部文件在这里? 如果variables包含引号,我需要转义吗?
2)使用cat&sedreplace每个variables的值:
鉴于template.txt:
The number is ${i} The word is ${word}
命令:
cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"
对我来说似乎不好,因为需要摆脱许多不同的符号,而且有许多变数,线条太长了。
你能想到一些其他优雅和安全的解决scheme吗?
你可以使用这个:
perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt
用相应的环境variablesreplace所有的${...}
string(不要忘记在运行这个脚本之前导出它们)。
对于纯粹的bash这应该工作(假设variables不包含$ {…}string):
#!/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引用某个引用自身的variables,则不会挂起的解决scheme:
#!/斌/庆典 line =“$(cat; echo -na)” end_offset = $ {#线} while([* $ {line:0:$ end_offset})=〜(。*)(\ $ \ {([a-zA-Z _] [a-zA-Z_0-9] *)\})(。* )]]; 做 PRE = “$ {BASH_REMATCH [1]}” POST = “$ {BASH_REMATCH [4]} $ {行:$ end_offset:$ {#线}}” VARNAME = “$ {BASH_REMATCH [3]}” eval'VARVAL =“$'$ VARNAME'”' 行= “$ PRE $ VARVAL $ POST” end_offset = $ {#PRE} DONE echo -n“$ {line:0:-1}”
警告 :我不知道在bash中正确处理NULinput的方法,或者保留后续换行符的数量。 最后的变体是因为它是因为贝壳“爱”二进制input:
-
read
将解释反斜杠。 -
read -r
不会解释反斜杠,但如果不以换行符结束,仍然会放弃最后一行。 -
"$(…)"
将会删除跟随的换行符,所以我以…
结束; echo -na
; echo -na
并使用echo -n "${line:0:-1}"
:这将丢弃最后一个字符(它是a
),并保留与input中的换行符一样多的尾随换行符(包括no)。
试试envsubst
FOO=foo BAR=bar export FOO BAR envsubst <<EOF FOO is $FOO BAR is $BAR EOF
envsubst对我来说是新的。 太棒了。
为了logging,使用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
我同意使用sed:这是search/replace的最佳工具。 这是我的方法:
$ 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
编辑2017年1月6日
我需要在我的configuration文件中保留双引号,所以双seeding双引号与sed帮助:
render_template() { eval "echo \"$(sed 's/\"/\\\\"/g' $1)\"" }
我想不出保留新的行,但保持两者之间的空行。
虽然这是一个老话题,国际海事组织我在这里find了更优雅的解决scheme: 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égoryPakosz 。
我认为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 provoded一个解决scheme,避免bash报价逃避问题:
$ eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
编辑:删除部分关于以root身份运行sudo …
编辑:添加评论关于如何引号需要逃脱,添加plockc的解决scheme的混合!
我这样做,可能效率较低,但更容易阅读/维护。
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
接受的答案更长但更强大的版本:
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}
所有实例展开为它们的环境值(或者,如果它们未定义,则为空string)。
它正确地逃避反斜杠,并接受一个反斜线转义$来禁止替代(不像envsubst,事实certificate, 这并不是这样做 )。
所以,如果你的环境是:
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的解决scheme,如mogsie,但与heredoc而不是herestring,让您避免双引号
eval "cat <<EOF $(<template.txt) EOF " 2> /dev/null
如果你想使用Jinja2模板,请看这个项目: j2cli 。
它支持:
- 来自JSON,INI,YAML文件和inputstream的模板
- 从环境variables模板化
从ZyX使用纯bash的答案,但与新风格正则expression式匹配和间接参数replace它变成:
#!/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
这个页面用awk描述了一个答案
awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
shtpl的完美案例。 (我的项目,所以没有被广泛的使用,缺less文档,但是这是它提供的解决scheme,可能你想testing它。)
只要执行:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
结果是:
the number is 1 the word is dog
玩的开心。
如果使用Perl是一个选项,并且仅仅基于环境variables的扩展(而不是所有的shellvariables),那么可以考虑Stuart P. Bentley强有力的答案 。
这个答案的目的是提供一个bash-only解决scheme ,尽pipe使用eval
,应该是安全的 。
目标是:
- 支持扩展
${name}
和$name
variables引用。 - 防止所有其他扩展:
- 命令replace(
$(...)
和传统语法`...`
) - 算术replace(
$((...))
和传统语法$[...]
)。
- 命令replace(
- 使用
\
(\${name}
)作为前缀,允许select性地抑制variables扩展。 - 保留特殊的字符。 在input,特别是
"
和\
实例。 - 允许通过参数或通过标准input。
函数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
- 出于性能方面的原因,该函数一次性读取stdininput到内存中,但很容易将这个函数调整为逐行方式。
- 也支持
${HOME:0:10}
类的非基本variables扩展,只要它们不包含embedded式命令或算术replace,如${HOME:0:$(echo 10)}
- 这样的embeddedreplace实际上是BREAK函数(因为所有的
$(
和`
实例都是一目了然的)。 - 同样,格式错误的variables引用(例如
${HOME
(missing closing))会使函数失效。
- 这样的embeddedreplace实际上是BREAK函数(因为所有的
- 由于bash对双引号string的处理,反斜杠的处理如下:
-
\$name
防止扩展。 - 没有跟在
$
之后的一个\
是保留原样。 - 如果你想代表多个相邻的
\
实例,你必须加倍 ; 例如:-
\\
– – >\
– 就像\
-
\\\\
\\
-
- input不能包含用于内部目的的以下(很less使用的)字符:
0x3
。
-
- 有一个很大的假设关注,如果bash应该引入新的扩展语法,这个函数可能不会阻止这种扩展 – 请参阅下面的解决scheme,不使用
eval
。
如果你正在寻找一个只支持${name}
扩展的更加严格的解决scheme,也就是说,使用强制大括号,忽略$name
引用 – 请参阅我的答案 。
以下是来自公认的答案的改进版本的bash-only, eval
-free解决scheme :
改进是:
- 支持扩展
${name}
和$name
variables引用。 - 支持不应该展开的
\
-escapingvariables引用。 - 与上述基于
eval
的解决scheme不同,- 非基本的扩展被忽略
- 格式错误的variables引用被忽略(它们不会中断脚本)
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, ie, 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"
这是另一个纯粹的bash解决scheme:
- 它使用heredoc,所以:
- 复杂性不会由于additionaly所需的语法而增加
- 模板可以包含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>
你也可以使用bashible (内部使用上面/下面描述的评估方法)。
有一个例子,如何从多个部分生成一个HTML:
https://github.com/mig1984/bashible/tree/master/examples/templates
# 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" }
这是纯粹的bashfunction可以根据自己的喜好调整,用于生产,不应该打破任何input。 如果它打破了 – 让我知道。
这是一个保留空白的bash函数:
# Render a file in bash, ie 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
function(根据我的需要,但应该很容易修改):
- 跳过参数扩展(例如\ $ {VAR})。
- 支持$ {VAR}forms的参数扩展,但不支持$ VAR。
- 如果没有VARvariables,则用空stringreplace$ {VAR}。
- 仅支持名称中的az,AZ,0-9和下划线字符(不包括位于第一位的数字)。
有关模板的一种方法,请参阅我的答案 。