代码之家  ›  专栏  ›  技术社区  ›  Vladislav Rastrusny

bash模板化:如何用bash从模板构建配置文件?

  •  100
  • Vladislav Rastrusny  · 技术社区  · 14 年前

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

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

    1) How to replace ${} placeholders in a text file?

    模板:

    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"
    

    顺便问一句,我如何将输出重定向到外部文件?如果变量包含引号,我是否需要转义?

    2)使用cat&sed将每个变量替换为其值:

    给定的template.txt:

    The number is ${i}
    The word is ${word}
    

    命令:

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

    对我来说似乎很糟糕,因为需要转义许多不同的符号,并且有许多变量,这条线将会很长。

    你能想出其他优雅安全的解决办法吗?

    21 回复  |  直到 6 年前
        1
  •  54
  •   ZyX    10 年前

    您可以使用:

    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中正确处理nuls输入或保留尾随新行的数量。最后一个变量是因为shells__love_二进制输入:

    1. read 将解释反斜杠。
    2. read -r 不会解释反斜杠,但如果最后一行不以换行符结尾,它仍将删除。
    3. "$(…)" 会去掉尽可能多的尾随新行,所以我结束 … 具有 ; echo -n a 使用 echo -n "${line:0:-1}" :这将删除最后一个字符(即 a )并保留输入中的尾随新行数(包括no)。
        2
  •  102
  •   yottatsa    12 年前

    尝试 envsubst

    FOO=foo
    BAR=bar
    export FOO BAR
    
    envsubst <<EOF
    FOO is $FOO
    BAR is $BAR
    EOF
    
        3
  •  35
  •   Dan Garthwaite    12 年前

    envsubt对我来说是新的。好极了。

    对于记录来说,使用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
    
        4
  •  31
  •   Hai Vu    14 年前

    我同意使用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
    
        5
  •  17
  •   mogsie    10 年前

    我认为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可以执行任意代码。把它作为根目录运行是不可能的。模板中的引号需要转义,否则它们将被 eval .

    如果您愿意,也可以在这里使用文档 cat echo

    $ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
    

    @PlockC提供了一种避免bash引用转义问题的解决方案:

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

    编辑: 删除了有关使用sudo将此作为根目录运行的部分…

    编辑: 添加了关于如何转义引号的注释,并将PlockC的解决方案添加到组合中!

        6
  •  16
  •   plockc    9 年前

    我有一个类似mogsie的bash解决方案,但是使用HereDoc而不是HereString可以避免转义双引号。

    eval "cat <<EOF
    $(<template.txt)
    EOF
    " 2> /dev/null
    
        7
  •  16
  •   CKK    8 年前

    编辑:2017年1月6日

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

    render_template() {
      eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
    }
    

    我不想一直跟在新行后面,但中间的空行是保留的。


    虽然这是一个古老的话题,但我在这里找到了更优雅的解决方案: 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 .

        8
  •  9
  •   Stuart P. Bentley    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
    
        9
  •  8
  •   Craig552uk    14 年前

    我本来可以这样做的,可能效率较低,但更容易阅读/维护。

    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
    
        10
  •  6
  •   kolypto    10 年前

    如果你想用 Jinja2 模板,请参见此项目: j2cli .

    它支持:

    • 来自json、ini、yaml文件和输入流的模板
    • 从环境变量模板化
        11
  •  6
  •   smentek    6 年前

    而不是重新设计车轮 envsubst 几乎可以用于任何场景,例如从Docker容器中的环境变量构建配置文件。

    如果在Mac上,请确保 homebrew 然后从gettext链接它:

    brew install gettext
    brew link --force gettext
    

    /模板

    # We put env variables into placeholders here
    this_variable_1 = ${SOME_VARIABLE_1}
    this_variable_2 = ${SOME_VARIABLE_2}
    

    SOME_VARIABLE_1=value_1
    SOME_VARIABLE_2=value_2
    

    /配置文件

    #!/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
    
        12
  •  5
  •   wich    10 年前

    使用纯bash从zyx获得答案,但使用新的风格regex匹配和间接参数替换,它变成:

    #!/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
    
        13
  •  4
  •   Community rcollyer    7 年前

    如果使用 珀尔 是一个选项,您满足于将扩展建立在 环境 仅变量 (与所有 变量) 考虑 Stuart P. Bentley's robust answer .

    此答案旨在提供 仅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 防止膨胀。
      • 单一的 \ 后面没有 $ 保持原样。
      • 如果你想代表 多个相邻 \ 实例,您必须 把它们加倍 例如:
        • \\ -gt; \ -和刚才一样 \
        • \\\\ -gt; \\
      • 输入不能包含以下(很少使用)字符,这些字符用于内部目的: 0x1 , 0x2 , 0x3 .
    • 有一个很大的假设性问题是,如果bash应该引入新的扩展语法,那么这个函数可能不会阻止这样的扩展——请参阅下面的解决方案,它不使用 埃瓦 .

    如果你在找一个 更严格的解决方案 只有 支架 ${No.} 扩张 -即 强制性的 大括号,忽略 美元名称 参考资料-见 this answer 我的。


    这里是一个 只改进了bash版本, 埃瓦 -免费的解决方案 accepted answer :

    改进包括:

    • 支持两者的扩展 ${No.} 美元名称 变量引用。
    • 支持 \ -转义不应扩展的变量引用。
    • 不像 埃瓦 -基于上述解决方案,
      • 非碱性的 扩展被忽略
      • 忽略格式错误的变量引用(它们不会破坏脚本)
     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"
    
        14
  •  4
  •   TomáÅ¡ PospíÅ¡ek    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 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>
    
        15
  •  3
  •   Matt Brown    12 年前

    本页描述了 answer with awk

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

    完美的案例 shtpl . (矿山项目,使用不广泛,缺乏文件。但无论如何,这就是它提供的解决方案。希望您能测试一下。)

    只需执行:

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

    结果是:

    the number is 1
    the word is dog
    

    玩得高兴。

        17
  •  3
  •   Hai Vu    6 年前

    另一个解决方案是:生成一个包含模板文件中所有变量和内容的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中,我们生成 cat 带Heredoc的命令
    • 最后,我们将子shell输出馈送给bash并生成所需的输出。
    • 如果要将此输出重定向到文件中,请将最后一行替换为:

      ) | bash > output.txt
      
        18
  •  1
  •   Jan Molič    9 年前

    您也可以使用 巴什布尔 (内部使用上述/下文所述的评估方法)。

    例如,如何从多个部分生成HTML:

    https://github.com/mig1984/bashible/tree/master/examples/templates

        19
  •  1
  •   ttt    7 年前
    # 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函数,可根据您的喜好进行调整,用于生产,不应中断任何输入。 如果坏了-告诉我。

        20
  •  0
  •   Igor Katson    9 年前

    下面是一个保持空白的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}"
    }
    
        21
  •  0
  •   Kevin    8 年前

    这是修改过的 perl 根据其他几个答案编写脚本:

    perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template
    

    功能(根据我的需要,但应易于修改):

    • 跳过转义参数扩展(例如\$var)。
    • 支持$var形式的参数扩展,但不支持$var形式的参数扩展。
    • 如果没有var envar,则将$var替换为空字符串。
    • 仅支持名称中的a-z、a-z、0-9和下划线字符(不包括第一个位置的数字)。