【问题标题】:How to replace ${} placeholders in a text file?如何替换文本文件中的 ${} 占位符?
【发布时间】:2010-09-29 18:19:21
【问题描述】:

我想将“模板”文件的输出通过管道传输到 MySQL,该文件中散布着像 ${dbName} 这样的变量。替换这些实例并将输出转储到标准输出的命令行实用程序是什么?

【问题讨论】:

    标签: bash command-line text-processing templating


    【解决方案1】:

    如果您愿意使用Perl,那将是我的建议。尽管可能有一些sed 和/或AWK 专家可能知道如何更容易地做到这一点。如果您有一个更复杂的映射,而不仅仅是 dbName 用于替换,您可以很容易地扩展它,但此时您最好将它放入标准 Perl 脚本中。

    perl -p -e 's/\$\{dbName\}/testdb/s' yourfile | mysql
    

    一个简短的 Perl 脚本来做一些稍微复杂的事情(处理多个键):

    #!/usr/bin/env perl
    my %replace = ( 'dbName' => 'testdb', 'somethingElse' => 'fooBar' );
    undef $/;
    my $buf = <STDIN>;
    $buf =~ s/\$\{$_\}/$replace{$_}/g for keys %replace;
    print $buf;
    

    如果你把上面的脚本命名为replace-script,那么它可以如下使用:

    replace-script < yourfile | mysql
    

    【讨论】:

    • 适用于单个变量,但我如何为其他变量包含“或”?
    • 您可以通过多种方式使用 perl 执行此操作,所有这些都取决于您想要执行此操作的复杂程度和/或安全程度。更复杂的例子可以在这里找到:perlmonks.org/?node_id=718936
    • 使用 perl 比尝试使用 shell 要干净得多。花时间完成这项工作,而不是尝试其他一些提到的基于 shell 的解决方案。
    • 最近不得不解决一个类似的问题。最后我选择了 perl(envsubst 看起来很有希望,但它太难控制了)。
    【解决方案2】:

    使用/bin/sh。创建一个设置变量的小型 shell 脚本,然后使用 shell 本身解析模板。像这样(编辑以正确处理换行符):

    文件模板.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"
    

    输出:

    #sh script.sh
    the number is 1
    the word is dog
    

    【讨论】:

    • 为什么不只是:while read line ;执行 eval echo "$line";完成
    • 这很危险!输入中的所有bash 命令都将被执行。如果模板是:"the words is; rm -rf $HOME" 你会丢失文件。
    • @rzymek - 请记住,他想将此文件直接通过管道传输到数据库。如此看来,输入是可信的。
    • @gnud 足够信任一个文件来存储它的内容和足够信任它来执行它包含的任何东西是有区别的。
    • 注意约束:(a)输入中的双引号被悄悄地丢弃,(b)read 命令,如所写,修剪每行的前导和尾随空格并“吃掉”@ 987654327@ 字符,(c) 仅在您完全信任或控制输入时使用它,因为嵌入在输入中的命令替换(`…` $(…))允许由于使用 eval 而执行任意命令。最后,echo 极有可能将行首误认为是它的命令行选项之一。
    【解决方案3】:

    如果您可以控制配置文件格式,则可以在 bash 本身中完成。您只需要 source (".") 配置文件而不是 subshel​​l。这确保变量是在当前 shell 的上下文中创建的(并继续存在)而不是子 shell(当子 shell 退出时变量消失)。

    $ cat config.data
        export parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA
        export parm_user=pax
        export parm_pwd=never_you_mind
    
    $ cat go.bash
        . config.data
        echo "JDBC string is " $parm_jdbc
        echo "Username is    " $parm_user
        echo "Password is    " $parm_pwd
    
    $ bash go.bash
        JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
        Username is     pax
        Password is     never_you_mind
    

    如果您的配置文件不能是 shell 脚本,您可以在执行之前“编译”它(编译取决于您的输入格式)。

    $ cat config.data
        parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA # JDBC URL
        parm_user=pax                              # user name
        parm_pwd=never_you_mind                    # password
    
    $ cat go.bash
        cat config.data
            | sed 's/#.*$//'
            | sed 's/[ \t]*$//'
            | sed 's/^[ \t]*//'
            | grep -v '^$'
            | sed 's/^/export '
            >config.data-compiled
        . config.data-compiled
        echo "JDBC string is " $parm_jdbc
        echo "Username is    " $parm_user
        echo "Password is    " $parm_pwd
    
    $ bash go.bash
        JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
        Username is     pax
        Password is     never_you_mind
    

    在您的具体情况下,您可以使用以下内容:

    $ cat config.data
        export p_p1=val1
        export p_p2=val2
    $ cat go.bash
        . ./config.data
        echo "select * from dbtable where p1 = '$p_p1' and p2 like '$p_p2%' order by p1"
    $ bash go.bash
        select * from dbtable where p1 = 'val1' and p2 like 'val2%' order by p1
    

    然后将 go.bash 的输出通过管道传输到 MySQL 中,瞧,希望你不会破坏你的数据库 :-)。

    【讨论】:

    • 您不必从 config.data 文件中导出变量;只需设置它们就足够了。您似乎也没有在任何时候阅读模板文件。或者,也许,模板文件被修改并包含“回声”操作......或者我错过了什么?
    • 关于导出的好点,我默认这样做是为了它们可用于子shell,并且不会造成任何伤害,因为它们在 go 退出时死亡。 “模板”文件是脚本本身及其 echo 语句。没有必要引入第三个文件——它基本上是一个邮件合并类型的操作。
    • “脚本本身及其 echo 语句”不是模板:它是脚本。想想 和 echo '' 之间的可读性(和可维护性)差异
    • @Pierre,我的配置脚本中没有 echo 语句,它们只是导出,我已经展示了如何通过最少的预处理来避免这种情况。如果您在谈论我的其他脚本中的 echo 语句(例如 go.bash),那么您就搞错了——它们不是解决方案的一部分,它们只是一种表明变量设置正确。
    • @paxdiablo :看来您只是忘记了这个问题:>。所以使用模板是个问题,它不是“大错特错”。导出变量并在另一个脚本中回显它们并不能回答问题根本
    【解决方案4】:

    Sed!

    给定模板.txt:

    数字是 ${i} 这个词是 ${word}

    我们只需要说:

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

    感谢 Jonathan Leffler 提供的将多个 -e 参数传递给同一 sed 调用的提示。

    【讨论】:

    • 您可以将这两个 sed 命令合二为一: sed -e "s/\${i}/1/" -e "s/\${word}/dog/";那更有效。您可能会在 100 次此类操作时遇到某些版本的 sed 问题(几年前的问题 - 可能仍然不正确,但要注意 HP-UX)。
    • 小提示:如果给定示例中的“1”或“dog”包含美元符号,则必须使用反斜杠对其进行转义(否则不会发生替换)。
    • 您也不需要cat。您只需要sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text
    • 如果替换文本是密码怎么办?在这种情况下,sed 将需要一个转义文本,这很麻烦。
    • 要将结果写入文本文件,您可以使用sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text | tee newFile
    【解决方案5】:

    模板.txt

    Variable 1 value: ${var1}
    Variable 2 value: ${var2}
    

    数据.sh

    #!/usr/bin/env bash
    declare var1="value 1"
    declare var2="value 2"
    

    解析器.sh

    #!/usr/bin/env bash
    
    # args
    declare file_data=$1
    declare file_input=$2
    declare file_output=$3
    
    source $file_data
    eval "echo \"$(< $file_input)\"" > $file_output
    

    ./parser.sh data.sh template.txt parsed_file.txt

    parsed_file.txt

    Variable 1 value: value 1
    Variable 2 value: value 2
    

    【讨论】:

    • 正如其他地方所指出的:仅在您完全信任或控制输入时才使用此选项,因为输入中嵌入的命令替换(`…` $(…))允许执行由于使用而导致的任意命令eval,以及由于使用 source 而直接执行的 shell 代码。此外,输入中的双引号会被悄悄丢弃,echo 可能会将行首误认为是它的命令行选项之一。
    • 不幸的是,这会从结果文件中删除所有双引号 (")。有没有办法在不删除双引号的情况下做同样的事情?
    • 我在这里找到了我要找的东西:stackoverflow.com/a/11050943/795158;我使用了 envsubst。不同之处在于必须导出变量,这对我来说没问题。
    • 如果文本文件包含“`”或“.” ,替代将失败。
    【解决方案6】:

    我在想同样的事情时发现了这个帖子。它启发了我(小心反引号)

    $ echo $MYTEST
    pass!
    $ cat FILE
    hello $MYTEST world
    $ eval echo `cat FILE`
    hello pass! world
    

    【讨论】:

    • $(cat file) 的 bash 简写是 $(&lt; file)
    • 显然这种方法弄乱了换行符,即我的文件在一行中全部回显。
    • @ArthurCorenzan:确实,换行符被替换为空格。要解决此问题,您必须使用 eval echo "\"$(cat FILE)\"" 但这可能仍然不足,因为输入中的双引号被丢弃。
    • 正如其他地方所指出的:仅在您完全信任或控制输入时才使用此选项,因为输入中嵌入的命令替换(`…` $(…))允许执行由于使用而导致的任意命令eval.
    【解决方案7】:

    考虑到最近的兴趣,我又在考虑这个问题,我认为我最初想到的工具是m4,自动工具的宏处理器。因此,您可以使用以下变量,而不是我最初指定的变量:

    $echo 'I am a DBNAME' | m4 -DDBNAME="database name"
    

    【讨论】:

    • 此解决方案在此处的答案中具有最少的缺点。你知道有什么方法可以替换 ${DBNAME} 而不是只替换 DBNAME 吗?
    • @JackDavidson 我将使用envsubst 进行这种简单的变量替换/模板用法,如其他答案中所述。 m4 是一个很棒的工具,但它是一个成熟的预处理器,具有更多的功能和复杂性,如果您只是想替换一些变量,则可能不需要。
    【解决方案8】:

    文件.tpl:

    The following bash function should only replace ${var1} syntax and ignore 
    other shell special chars such as `backticks` or $var2 or "double quotes". 
    If I have missed anything - let me know.
    

    script.sh:

    template(){
        # usage: template file.tpl
        while read -r line ; do
                line=${line//\"/\\\"}
                line=${line//\`/\\\`}
                line=${line//\$/\\\$}
                line=${line//\\\${/\${}
                eval "echo \"$line\""; 
        done < ${1}
    }
    
    var1="*replaced*"
    var2="*not replaced*"
    
    template file.tpl > result.txt
    

    【讨论】:

    • 这不安全,因为它会在模板中执行命令替换,如果它们有一个前导反斜杠,例如\$(date)
    • 除了 Peter 的有效观点之外:我建议您使用 while IFS= read -r line; do 作为 read 命令,否则您将删除每个输入行的前导和尾随空格。此外,echo 可能会将行首误认为是它的命令行选项之一,因此最好使用printf '%s\n'。最后,双引号 ${1} 更安全。
    【解决方案9】:

    这是我基于以前答案的 perl 解决方案,替换了环境变量:

    perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1}?$ENV{$1}:"missing variable $1")/eg' < infile > outfile
    

    【讨论】:

    • 这很棒。不要总是有 perl,但当你这样做时,这很简单直接。
    【解决方案10】:

    更新

    这是来自 yottatsa 的一个类似问题的解决方案,它只替换 $VAR 或 ${VAR} 等变量,并且是一个简短的单行代码

    i=32 word=foo envsubst < template.txt
    

    当然,如果iword在你的环境中,那么它只是

    envsubst < template.txt
    

    在我的 Mac 上,它看起来像是作为 gettext 的一部分并从 MacGPG2

    安装的

    旧答案

    这是对mogsie 对类似问题的解决方案的改进,我的解决方案不需要你升级双引号,mogsie 需要,但他是单引号!

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

    这两种解决方案的强大之处在于,您只会得到几种通常不会发生的 shell 扩展 $((...))、`...` 和 $(...),尽管反斜杠这里是转义字符,不过不用担心解析有bug,多行就好了。

    【讨论】:

    • @ToddiusZho:没有不导出的环境变量之类的东西——正是导出使 shell 变量成为环境变量。 envsubst,顾名思义,只识别 environment 变量,而不识别 shell 变量。还值得注意的是,envsubst 是一个 GNU 实用程序,因此并非在所有平台上都预装或可用。
    • 也许另一种说法是 envsubst 只看到它自己的进程环境变量,因此您可能之前定义的“正常”shell 变量(在单独的行上)不会被子进程继承,除非您“导出“ 他们。在我上面使用 gettext 的示例中,我通过 bash 机制修改继承的 gettext 环境,方法是将它们添加到我将要运行的命令的前缀
    • 我有一个带有 $HOME 的字符串,我发现 $HOME 作为默认 shell 工作,而不是 $HOME 作为我自己的 /home/zw963,但是,它似乎不支持 $( cat /etc/hostname) 替换,所以它不完全符合我自己的需求。
    • 感谢您的“旧答案”,因为它不仅允许变量,还允许像 $(ls -l) 这样的 shell 命令
    【解决方案11】:

    这里有很多选择,但我想我会把我的扔掉。它基于 perl,仅针对 ${...} 形式的变量,将要处理的文件作为参数并将转换后的文件输出到 stdout:

    use Env;
    Env::import();
    
    while(<>) { $_ =~ s/(\${\w+})/$1/eeg; $text .= $_; }
    
    print "$text";
    

    当然,我不是一个真正的 perl 人,所以很容易出现致命缺陷(虽然对我有用)。

    【讨论】:

    • 工作正常。您可以删除 Env::import(); 行 - use 暗示导入。另外,我建议不要先在内存中构建整个输出:只需在循环内使用print; 而不是$text .= $_;,然后删除循环后print 命令。
    【解决方案12】:

    我建议使用 Sigil 之类的东西: https://github.com/gliderlabs/sigil

    它被编译为单个二进制文件,因此在系统上安装非常容易。

    然后你可以像下面这样做一个简单的单行:

    cat my-file.conf.template | sigil -p $(env) > my-file.conf
    

    这比eval 安全得多,并且比使用正则表达式或sed 更容易

    【讨论】:

    • 很好的答案!这是一个合适的模板系统,比其他答案更容易使用。
    • 顺便说一句,最好避免使用cat,而是使用&lt;my-file.conf.template,这样你就可以给sigil一个真正的文件句柄而不是FIFO。
    【解决方案13】:

    这是一个强大的 Bash 函数,尽管使用了 eval,但应该可以安全使用。

    输入文本中的所有${varName} 变量引用都基于调用shell 的变量进行扩展。

    Nothing else 被扩展:既没有 包含在{...} 中的变量引用(例如$varName),也没有命令替换($(...) 和旧语法 `...`),也不是算术替换($((...)) 和旧语法 $[...])。

    要将$ 视为文字,\-将其转义;例如:\${HOME}

    请注意,输入只能通过 stdin 接受。

    示例:

    $ expandVarsStrict <<<'$HOME is "${HOME}"; `date` and \$(ls)' # only ${HOME} is expanded
    $HOME is "/Users/jdoe"; `date` and $(ls)
    

    函数源代码:

    expandVarsStrict(){
      local line lineEscaped
      while IFS= read -r line || [[ -n $line ]]; do  # the `||` clause ensures that the last line is read even if it doesn't end with \n
        # Escape ALL chars. that could trigger an expansion..
        IFS= read -r -d '' lineEscaped < <(printf %s "$line" | tr '`([$' '\1\2\3\4')
        # ... then selectively reenable ${ references
        lineEscaped=${lineEscaped//$'\4'{/\${}
        # Finally, escape embedded double quotes to preserve them.
        lineEscaped=${lineEscaped//\"/\\\"}
        eval "printf '%s\n' \"$lineEscaped\"" | tr '\1\2\3\4' '`([$'
      done
    }
    

    该函数假定输入中不存在0x10x20x30x4 控制字符,因为这些字符。在内部使用 - 因为函数处理 text,这应该是一个安全的假设。

    【讨论】:

    • 这是这里最好的答案之一。即使使用eval 也很安全。
    • 此解决方案适用于 JSON 文件! (正确转义"!)
    • 这个解决方案的一个好处是它可以让你为缺少的变量提供默认值 ${FOO:-bar} 或者只在设置时输出一些东西 - ${HOME+Home is ${HOME}} 。我怀疑通过一点扩展它也可以返回缺少变量的退出代码${FOO?Foo is missing},但如果有帮助,tldp.org/LDP/abs/html/parameter-substitution.html 目前没有这些列表
    【解决方案14】:

    创建rendertemplate.sh:

    #!/usr/bin/env bash
    
    eval "echo \"$(cat $1)\""
    

    还有template.tmpl:

    Hello, ${WORLD}
    Goodbye, ${CHEESE}
    

    渲染模板:

    $ export WORLD=Foo
    $ CHEESE=Bar ./rendertemplate.sh template.tmpl 
    Hello, Foo
    Goodbye, Bar
    

    【讨论】:

    • 这会去掉双引号字符串
    • 试过:eval "echo $(cat $1)" - 没有引号,它对我有用。
    • 从安全角度来看,这是个坏消息。如果您的模板包含$(rm -rf ~),则您将其作为代码运行。
    • eval "echo \"$(cat $1)\"" 效果很好!
    【解决方案15】:

    这是一种让 shell 为您进行替换的方法,就好像文件的内容是在双引号之间键入的一样。

    使用带有内容的template.txt示例:

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

    以下行将导致 shell 插入 template.txt 的内容并将结果写入标准输出。

    i='1' word='dog' sh -c 'echo "'"$(cat template.txt)"'"'
    

    解释:

    • iword 作为环境变量传递给 sh 的执行。
    • sh 执行它所传递的字符串的内容。
    • 彼此相邻写入的字符串变成一个字符串,该字符串是:
      • 'echo "' + "$(cat template.txt)" + '"'
    • 由于替换在" 之间,“$(cat template.txt)”成为cat template.txt 的输出。
    • 所以sh -c执行的命令变成:
      • echo "The number is ${i}\nThe word is ${word}",
      • 其中iword 是指定的环境变量。

    【讨论】:

    • 从安全角度来看,这是个坏消息。如果您的模板包含'$(rm -rf ~)'$(rm -rf ~),则模板文件中的文字引号将与您在扩展之前添加的引号相匹配。
    • 我的模板内引号不匹配模板外引号,我相信 shell 正在独立解析模板和终端字符串(有效删除引号)然后将它们连接起来。不删除主目录的测试版本是'$(echo a)'$(echo a)。它产生'a'a。发生的主要事情是' 中的第一个echo a 正在被评估,这可能不是您所期望的,因为它在' 中,但与在" 中包含' 的行为相同带引号的字符串。
    • 因此,这在某种意义上是不安全的,因为它允许模板作者执行他们的代码。但是,如何评估报价并不会真正影响安全性。扩展任何 " 引用的字符串(包括 $(...))是重点。
    • 这就是重点吗?我只看到他们要求${varname},而不是其他更高安全风险的扩展。
    • ...也就是说,我必须有所不同(重新:模板内和模板外引号能够匹配)。当您在字符串中加入单引号时,您将拆分为单引号字符串echo ",然后是一个双引号字符串,其文字内容为template.txt,然后是另一个文字字符串",所有连接成一个参数传递给sh -c。你是对的 ' 不能匹配(因为它被外壳消耗而不是传递给内部),但是 " 肯定可以,所以可以执行包含 Gotcha"; rm -rf ~; echo " 的模板.
    【解决方案16】:

    使用 perl 编辑可能的多个文件,并带有备份。

      perl -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : ""/eg' \
        -i.orig \
        -p config/test/*
    

    【讨论】:

      【解决方案17】:

      我创建了一个名为 shtpl 的 shell 模板脚本。我的 shtpl 使用类似 jinja 的语法,现在我经常使用 ansible,我非常熟悉:

      $ cat /tmp/test
      {{ aux=4 }}
      {{ myarray=( a b c d ) }}
      {{ A_RANDOM=$RANDOM }}
      $A_RANDOM
      {% if $(( $A_RANDOM%2 )) == 0 %}
      $A_RANDOM is even
      {% else %}
      $A_RANDOM is odd
      {% endif %}
      {% if $(( $A_RANDOM%2 )) == 0 %}
      {% for n in 1 2 3 $aux %}
      \$myarray[$((n-1))]: ${myarray[$((n-1))]}
      /etc/passwd field #$n: $(grep $USER /etc/passwd | cut -d: -f$n)
      {% endfor %}
      {% else %}
      {% for n in {1..4} %}
      \$myarray[$((n-1))]: ${myarray[$((n-1))]}
      /etc/group field #$n: $(grep ^$USER /etc/group | cut -d: -f$n)
      {% endfor %}
      {% endif %}
      
      
      $ ./shtpl < /tmp/test
      6535
      6535 is odd
      $myarray[0]: a
      /etc/group field #1: myusername
      $myarray[1]: b
      /etc/group field #2: x
      $myarray[2]: c
      /etc/group field #3: 1001
      $myarray[3]: d
      /etc/group field #4: 
      
      

      关于我的github的更多信息

      【讨论】:

        【解决方案18】:

        对我来说这是最简单、最强大的解决方案,您甚至可以使用相同的命令 eval echo "$(&lt;template.txt) 包含其他模板:

        嵌套模板示例

        1. 创建模板文件,变量为常规bash语法${VARIABLE_NAME}$VARIABLE_NAME

        您必须在模板中使用\ 转义特殊字符,否则它们将被eval 解释。

        template.txt

        Hello ${name}!
        eval echo $(<nested-template.txt)
        

        nested-template.txt

        Nice to have you here ${name} :\)
        
        1. 创建源文件

        template.source

        declare name=royman 
        
        1. 解析模板
        source template.source && eval echo "$(<template.txt)"
        
        1. 输出
        Hello royman!
        Nice to have you here royman :)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-12-20
          • 1970-01-01
          • 2023-03-12
          • 2012-12-07
          • 1970-01-01
          • 2013-11-15
          • 2022-11-25
          相关资源
          最近更新 更多