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

Vla*_*sny 118 bash templates templating

我正在编写一个脚本来自动为我自己的web服务器创建Apache和PHP的配置文件.我不想使用像CPanel或ISPConfig这样的任何GUI.

我有一些Apache和PHP配置文件的模板.Bash脚本需要读取模板,进行变量替换并将解析后的模板输出到某个文件夹中.最好的方法是什么?我可以想到几种方法.哪一个是最好的还是有更好的方法可以做到这一点?我想在纯Bash中做到这一点(例如在PHP中很容易)

1)如何在文本文件中替换$ {}占位符?

template.txt:

the number is ${i}
the word is ${word}
Run Code Online (Sandbox Code Playgroud)

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"
Run Code Online (Sandbox Code Playgroud)

顺便说一句,如何在此处将输出重定向到外部文件?如果变量包含引号,我是否需要逃避某些事情?

2)使用cat&sed替换每个变量的值:

给出template.txt:

The number is ${i}
The word is ${word}
Run Code Online (Sandbox Code Playgroud)

命令:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"
Run Code Online (Sandbox Code Playgroud)

对我来说似乎不好,因为需要逃避许多不同的符号,并且对于许多变量,这条线太长了.

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

小智 121

尝试 envsubst

FOO=foo
BAR=bar
export FOO BAR

envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF
Run Code Online (Sandbox Code Playgroud)

  • 仅供参考,使用heredoc时不需要`envsubst`,因为bash将heredoc视为文字双引号字符串并在其中插入变量.当你想从另一个文件中读取模板时,这是一个很好的选择.对于更麻烦的`m4`来说,这是一个很好的替代品. (10认同)
  • 注意:`envsubst`是一个GNU gettext实用程序,实际上并不是那么健壮(因为gettext用于本地化人类消息).最重要的是,它无法识别反斜杠转义的$ {VAR}替换(因此您无法在运行时使用$ VAR替换模板,如shell脚本或Nginx conf文件).有关处理反斜杠转义的解决方案,请参阅[我的回答](http://stackoverflow.com/a/25019138/34799). (4认同)
  • @beporter在这种情况下,如果你想将这个模板由于某种原因传递给envsubst,你需要使用`<<"EOF"`,*不*插入变量(引用的终止符就像单引号heredocs). (4认同)
  • 遗憾的是,不可能让 envsubst 因缺少环境变量而失败,这使得它不适合很多用例。 (3认同)
  • 了解这个命令让我非常惊喜.我试图手动拼凑envsubst的功能,但没有成功.谢谢yottatsa! (2认同)
  • 我用它像:`cat template.txt | envsubst` (2认同)

ZyX*_*ZyX 58

你可以用这个:

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt
Run Code Online (Sandbox Code Playgroud)

${...}用相应的环境变量替换所有字符串(不要忘记在运行此脚本之前导出它们).

对于纯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
Run Code Online (Sandbox Code Playgroud)

.如果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的输入或保留尾随换行的数量.最后一个变体是因为它是"爱"二进制输入:

  1. read 将解释反斜杠.
  2. read -r 不会解释反斜杠,但如果不以换行符结尾,仍然会删除最后一行.
  3. "$(…)"将去除尽可能多的尾随换行符则不存在,所以我最终; echo -n a和使用echo -n "${line:0:-1}":此下降的最后一个字符(这是a),并作为有在输入(包括无)保留尽可能多的后换行.

  • 注意:显然有一个从bash 3.1到3.2(及以上)的变化,其中正则表达式周围的单引号 - 将正则表达式的内容视为字符串文字.所以上面的正则表达式应该是......(\ $\{[a-zA-Z _] [a-zA-Z_0-9]*\})http://stackoverflow.com/questions/304864/how-do- I-使用正则表达式,在-的bash的脚本 (5认同)
  • 我会在bash版本中将`[^}]`更改为`[A-Za-Z _] [A-Za-z0-9_]`以防止shell超出严格替换(例如,如果它试图处理`$ {some_unused_var - $(rm -rf $ HOME)}`). (3认同)
  • @FractalizeR你可能想要在perl解决方案中将`$&`更改为``"`:如果它易于替换,则首先离开`$ {...}`,第二次用空字符串替换它. (2认同)
  • 要使 `while` 循环读取最后一行,即使它没有被换行符终止,请使用 `while read -r line || [[ -n $line ]]; 做`。此外,您的“read”命令会从每一行中去除前导和尾随空格;为避免这种情况,请使用 `while IFS= read -r line || [[ -n $line ]]; 做` (2认同)
  • 只是要注意那些寻求全面解决方案的人的约束:这些方便的解决方案不允许您有选择地保护变量引用免于扩展(例如通过`\`-escaping它们). (2认同)

Dan*_*ite 39

envsubst对我来说很新鲜.太棒了.

对于记录,使用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
Run Code Online (Sandbox Code Playgroud)

  • 我比 `envsubst` 更喜欢这个,因为它使我免于在 Dockerfile 中附加 `apt-get install gettext-base` (3认同)

Hai*_* Vu 31

我同意使用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
Run Code Online (Sandbox Code Playgroud)


mog*_*sie 23

我认为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"
Run Code Online (Sandbox Code Playgroud)

当然,应该谨慎使用此方法,因为eval可以执行任意代码.以root身份运行这几乎是不可能的.模板中的引号需要转义,否则它们将被吃掉eval.

如果您愿意cat,也可以使用此处的文档echo

$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
Run Code Online (Sandbox Code Playgroud)

@plockc提出了一个避免bash引用转义问题的解决方案:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Run Code Online (Sandbox Code Playgroud)

编辑:删除部分关于使用sudo以root身份运行它...

编辑:添加了关于如何转义报价的评论,添加了plockc的混合解决方案!


plo*_*ckc 19

我有像mogsie这样的bash解决方案但是使用heredoc而不是herestring来避免转义双引号

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Run Code Online (Sandbox Code Playgroud)

  • 此解决方案支持模板中的[Bash参数扩展](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html).我最喜欢的是_required parameters_ with`$ {param:?}`和嵌套文本_around_可选参数.示例:`$ {DELAY:+ <delay> $ DELAY </ delay>}`当DELAY未定义时扩展为空,当DELAY = 17时<delay> 17 </ delay>. (4认同)
  • 哦! 并且EOF定界符可以使用动态字符串,如PID`_EOF _ $$`. (2认同)

CKK*_*CKK 16

编辑2017年1月6日

我需要在配置文件中保留双引号,以便使用sed双重转义双引号有助于:

render_template() {
  eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}
Run Code Online (Sandbox Code Playgroud)

我想不到保留尾随的新行,但保留了两行之间的空行.


虽然这是一个老话题,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
Run Code Online (Sandbox Code Playgroud)

所有学分都归GrégoryPakosz所有.

  • 我需要两个较少的反斜杠来使其工作,即``eval'echo \"$(sed's/\"/ \\"/ g'$ 1)\""` (2认同)

Stu*_*ley 9

接受答案的更长但更强大的版本:

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
Run Code Online (Sandbox Code Playgroud)

这扩大所有实例$VAR ${VAR}他们的环境值(或者,如果他们是不确定的,空字符串).

它正确地逃避反斜杠,并接受反斜杠转义$来禁止替换(与envsubst不同,事实证明,它不会这样做).

因此,如果您的环境是:

FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi
Run Code Online (Sandbox Code Playgroud)

你的模板是:

Two ${TARGET} walk into a \\$FOO. \\\\
\\\$FOO says, "Delete C:\\Windows\\System32, it's a virus."
$BAZ replies, "\${NOPE}s."
Run Code Online (Sandbox Code Playgroud)

结果将是:

Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."
Run Code Online (Sandbox Code Playgroud)

如果你只想在$之前转义反斜杠(你可以在模板中写"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
Run Code Online (Sandbox Code Playgroud)


Cra*_*2uk 8

我这样做了,效率可能不高,但更容易阅读/维护.

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
Run Code Online (Sandbox Code Playgroud)

  • 你可以不用逐行阅读和只有一个sed调用来执行此操作:`sed -e's/VARONE/NEWVALA/g'-e's/VARTWO/NEWVALB/g'-e's/VARTHR/NEWVALC/g'<$ TEMPLATE> $ OUTPUT` (10认同)

kol*_*pto 8

如果您想使用Jinja2模板,请参阅此项目:j2cli.

它支持:

  • 来自JSON,INI,YAML文件和输入流的模板
  • 模仿环境变量


sme*_*tek 8

使用envsubst而不是重新发明轮子 几乎可以在任何场景中使用,例如从docker容器中的环境变量构建配置文件.

如果在mac上确保你有自制软件,那么从gettext链接它:

brew install gettext
brew link --force gettext
Run Code Online (Sandbox Code Playgroud)

./template.cfg

# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}
Run Code Online (Sandbox Code Playgroud)

./.env:

SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2
Run Code Online (Sandbox Code Playgroud)

./configure.sh

#!/bin/bash
cat template.cfg | envsubst > whatever.cfg
Run Code Online (Sandbox Code Playgroud)

现在只需使用它:

# 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
Run Code Online (Sandbox Code Playgroud)


Tom*_*šek 6

这是另一个纯 bash 解决方案:

  • 它使用heredoc,所以:
    • 由于额外需要的语法,复杂性不会增加
    • 模板可以包含 bash 代码
      • 这也允许您正确缩进内容。见下文。
  • 它不使用 eval,所以:
    • 渲染尾随空行没有问题
    • 模板中的引号没有问题

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  echo EOT
}

cat_template template | LISTING="$LISTING" bash
Run Code Online (Sandbox Code Playgroud)

$ cat template (带有尾随换行符和双引号)

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
$( echo "$LISTING" | sed 's/^/        /' )
      <pre>
    </p>
  </body>
</html>
Run Code Online (Sandbox Code Playgroud)

输出

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
        code
        template
      <pre>
    </p>
  </body>
</html>
Run Code Online (Sandbox Code Playgroud)


Hai*_* Vu 6

这是另一个解决方案:使用模板文件的所有变量和内容生成一个 bash 脚本,该脚本如下所示:

word=dog           
i=1                
cat << EOF         
the number is ${i} 
the word is ${word}

EOF                
Run Code Online (Sandbox Code Playgroud)

如果我们将此脚本输入 bash,它将产生所需的输出:

the number is 1
the word is dog
Run Code Online (Sandbox Code Playgroud)

以下是生成该脚本并将该脚本输入 bash 的方法:

(
    # Variables
    echo word=dog
    echo i=1

    # add the template
    echo "cat << EOF"
    cat template.txt
    echo EOF
) | bash
Run Code Online (Sandbox Code Playgroud)

讨论

  • 括号打开一个子shell,其目的是将所有生成的输出组合在一起
  • 在子 shell 中,我们生成所有变量声明
  • 同样在子 shell 中,我们cat使用 HEREDOC生成命令
  • 最后,我们将子 shell 输出提供给 bash 并产生所需的输出
  • 如果要将此输出重定向到文件中,请将最后一行替换为:

    ) | bash > output.txt
    
    Run Code Online (Sandbox Code Playgroud)


wic*_*ich 5

使用纯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
Run Code Online (Sandbox Code Playgroud)


mkl*_*nt0 5

如果使用Perl是一种选择,并且您满足于基于环境变量(而不是所有shell变量)进行扩展,请考虑Stuart P. Bentley 的可靠答案

这个答案旨在提供一个使用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' '`(['
}
Run Code Online (Sandbox Code Playgroud)

例子:

$ 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
Run Code Online (Sandbox Code Playgroud)
  • 出于性能原因,该函数一次所有stdin 输入读入内存,但很容易将该函数调整为逐行方法。
  • 还支持非基本变量扩展,例如${HOME:0:10},只要它们不包含嵌入式命令或算术替换,例如${HOME:0:$(echo 10)}
    • 这种嵌入的替换实际上是 BREAK 函数(因为所有$(`实例都被盲目地转义了)。
    • 类似地,格式错误的变量引用,例如${HOME(缺少关闭})BREAK 函数。
  • 由于 bash 对双引号字符串的处理,反斜杠处理如下:
    • \$name 防止扩张。
    • 一个\没有跟在后面的$被保留原样。
    • 如果要表示多个相邻的 \实例,则必须将它们加倍;例如:
      • \\-> \- 和刚才一样\
      • \\\\ -> \\
    • 输入不得包含以下(很少使用的)字符,这些字符用于内部用途:0x1, 0x2, 0x3.
  • 有一个在很大程度上假设的问题,如果 bash 应该引入新的扩展语法,这个函数可能不会阻止这样的扩展 - 请参阅下面的解决方案,不使用eval.

如果您正在寻找支持扩展更具限制性的解决方案${name}- 即,使用强制大括号,忽略$name引用 - 请参阅我的这个答案


这是已接受答案中仅 bash 的eval免费解决方案改进版本

改进之处是:

  • 支持扩展引用${name}$name变量引用。
  • 支持\转义不应扩展的变量引用。
  • eval上面基于 - 的解决方案不同,
    • 忽略非基本扩展
    • 格式错误的变量引用被忽略(它们不会破坏脚本)
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' '`(['
}
Run Code Online (Sandbox Code Playgroud)