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:

  1. read将解释反斜杠。
  2. read -r不会解释反斜杠,但如果不以换行符结束,仍然会放弃最后一行。
  3. "$(…)"将会删除跟随的换行符,所以我以结束; 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}$namevariables引用。
  • 防止所有其他扩展:
    • 命令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))会使函数失效。
  • 由于bash对双引号string的处理,反斜杠的处理如下:
    • \$name防止扩展。
    • 没有跟在$之后的一个\是保留原样。
    • 如果你想代表多个相邻的 \实例,你必须加倍 ; 例如:
      • \\ – – > \ – 就像\
      • \\\\ \\
    • input不能包含用于内部目的的以下(很less使用的)字符: 0x3
  • 有一个很大的假设关注,如果bash应该引入新的扩展语法,这个函数可能不会阻止这种扩展 – 请参阅下面的解决scheme,不使用eval

如果你正在寻找一个支持${name}扩展更加严格的解决scheme,也就是说,使用强制大括号,忽略$name引用 – 请参阅我的答案 。


以下是来自公认的答案改进版本的bash-only, eval -free解决scheme

改进是:

  • 支持扩展${name}$namevariables引用。
  • 支持不应该展开的\ -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和下划线字符(不包括位于第一位的数字)。

有关模板的一种方法,请参阅我的答案 。