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

188

我正在编写一个脚本来自动创建Apache和PHP的配置文件,用于我的Web服务器。我不想使用任何GUI,如CPanel或ISPConfig。

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

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

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"

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

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

给定template.txt(见上文)

命令:

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

我认为这样做不太好,因为需要转义很多不同的符号,而且如果有很多变量,那么这一行代码会非常长。

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


这个回答解决了你的问题吗?如何替换文本文件中的${}占位符? - Ky -
如果您有 PHP 这样的强大模板语言可用,那么“纯 Bash”要求似乎是不必要的。 - jchook
26个回答

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

这个命令会将所有的$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

7

这里是另一种解决方案:生成一个带有所有变量和模板文件内容的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

讨论

  • The parentheses opens a sub shell, its purpose is to group together all the output generated
  • Within the sub shell, we generate all the variable declarations
  • Also in the sub shell, we generate the cat command with HEREDOC
  • Finally, we feed the sub shell output to bash and produce the desired output
  • If you want to redirect this output into a file, replace the last line with:

    ) | bash > output.txt
    

7
这里有另一种纯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

输入:
$ cat 模板(带有尾随的换行符和双引号)

<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>

5
如果使用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防止扩展;不跟随$的单个\保留原样;如果您想表示多个相邻的\实例,则必须将它们加倍;例如:\\->\ -与\相同;\\\\->\\。输入不能包含以下(很少使用的)字符,这些字符用于内部目的:0x1、0x2、0x3。有一个大多是假设性的担忧,即如果bash应该引入新的扩展语法,这个函数可能无法防止这样的扩展-请参见下面的解决方案,该解决方案不使用eval。
如果你正在寻找一个更加严格的解决方案,仅支持${name}扩展——即,强制使用花括号,忽略$name引用——请参见我的this answer
这是一个改进版的仅使用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"

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

4

为了跟进于此页面上plockc的答案,这里有一个适用于dash的版本,供那些想要避免bashism的人使用。

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

4

尝试使用shtpl

对于shtpl来说是一个完美的案例。(这是我自己的项目,所以它并没有被广泛使用,并且缺乏文档。但是它仍然提供了解决方案。也许你想测试一下。)

只需执行:

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

结果是:

the number is 1
the word is dog

玩得开心。


1
如果它很糟糕,那么它会收到负评。我对此感到满意。但好的,我明白了,它并不清楚地显示这实际上是我的项目。将来会让它更加明显。无论如何感谢您的评论和时间。 - zstegi
我想补充一下,昨天我真的搜索了很多应用场景,看看 shtpl 是否是一个完美的解决方案。是啊,我挺无聊的... - zstegi

3

这个页面描述了一个使用awk的答案

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

这会使所有引号保持不变。太好了! - Pepster

3
# 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函数,可根据您的喜好进行调整,在生产中使用,不应在任何输入上发生错误。如果出现问题请告诉我。


1

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

使用非常简单:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接