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


Answers:


192

塞德

给定template.txt:

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

我们只需要说:

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

感谢Jonathan Leffler的技巧,它可以将多个-e参数传递给同一sed调用。


13
您可以将这两个sed命令组合为一个:sed -e“ s / \ $ {i} / 1 /” -e“ s / \ $ {word} / dog /”; 效率更高。某些版本的sed可能会遇到这种问题,执行此类操作可能需要100次(几年前的问题-可能仍然不正确,但要当心HP-UX)。
乔纳森·莱夫勒

1
谢谢乔纳森,正是我想要的。
Dana the Sane

3
小提示:如果给定示例中的“ 1”或“ dog”包含美元符号,则必须用反斜杠对其进行转义(否则不会发生替换)。
MatthieuP

9
您也不需要cat。您所需要的就是sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text
HardlyKnowEm 2013年

3
如果替换文本是密码怎么办?在这种情况下,sed将期望转义的文本,这很麻烦。
jpbochi 2015年

177

更新资料

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

i=32 word=foo envsubst < template.txt

当然,如果iword在您的环境中,那只是

envsubst < template.txt

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

旧答案

这是对mogsie的解决方案的一个类似问题的改进,我的解决方案不需要escale双引号,mogsie的则是双引号,但他只是一个衬板!

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

这两种解决方案的强大之处在于,尽管反斜杠一个在这里转义字符,但是您不必担心解析存在错误,并且多行就可以了。


6
我发现envsubst如果不导出您的envar,则裸机不起作用。
Toddius Zho,2015年

4
@ToddiusZho:没有不导出的环境变量之类的东西-正是通过导出使shell变量成为环境变量。envsubst顾名思义,它只能识别环境变量,而不能识别shell变量。还值得注意的是,它envsubst是一个GNU实用程序,因此并非预安装或在所有平台上都可用。
mklement0

2
也许另一种说法是envsubst只看到它自己的进程环境变量,因此子进程不会继承您先前定义(在单独的行上)的“普通” shell变量,除非您“导出”它们。在上面的gettext的示例用法中,我正在通过bash机制修改继承的gettext环境,方法是在它们前面加上要运行的命令作为前缀
plockc 2015年

1
我有一个带有$ HOME的字符串,我发现$ HOME用作默认外壳,而将$ HOME用作我自己的/ home / zw963,但是,似乎不支持$(cat / etc / hostname)替换,因此它不完全符合我的需求。
zw963 '16

3
感谢“旧答案”,因为它不仅允许变量,而且还允许$(ls -l)这样的shell命令
Alek

46

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

文件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"

输出:

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

2
为什么不这样:在阅读行时;做eval echo“ $ line”; 完成<./template.txt ??? 无需将整个文件读入内存,只需通过大量使用头部和尾部一次将其吐出一行即可。但是'eval'是可以的-除非模板包含外壳字符(如反引号)。
乔纳森·勒夫勒

16
这非常危险!bash输入中的所有命令将被执行。如果模板是:“单词是; rm -rf $ HOME”,则将丢失文件。
rzymek

1
@rzymek-记住,他想将此文件直接通过管道传输到数据库。因此,显然,输入是可信的。
gnud 2013年

4
@gnud信任文件足以存储其内容和信任文件足以执行文件中包含的内容是有区别的。
2014年

3
要注意这些约束:(a)输入中的双引号被悄悄地丢弃,(b)read按照编写的命令,修剪每行的前导和尾随空格,并'吃掉' \ 字符。(c)仅在完全信任或控制输入,因为由于使用,嵌入在输入中的命令替换(`…` $(…))允许执行任意命令eval。最后,极少有机echo会将其命令行选项之一误认为行首。
mklement0 2015年

23

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

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

1
此解决方案具有此处答案最少的缺点。您是否知道有什么方法可以代替$ {DBNAME}而不是仅替换DBNAME?
杰克·戴维森

@JackDavidson我将使用envsubst此简单的变量替换/模板化用法,如其他答案中所述。m4虽然它是一个很好的工具,但是它是功能齐全的预处理器,具有更多的功能,因此如果您只想替换一些变量,则可能不需要复杂性。
imiric

13

template.txt

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

数据文件

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

解析器

#!/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

1
如其他地方所述:仅当您完全信任或控制输入时才使用此命令,因为嵌入在输入中的命令替换(`…` $(…))由于使用允许执行任意命令,而由于使用eval则可以直接执行shell代码source。同样,输入中的双引号会被悄悄地丢弃,并且echo可能会将其命令行选项之一误认为行首。
mklement0 2015年

不幸的是,这会从结果文件中删除所有双引号(“)。是否有一种方法可以在不删除双引号的情况下进行同样的操作?
Ivaylo Slavov

我在这里找到了想要的东西:stackoverflow.com/a/11050943/795158 ; 我用过envsubst。区别在于必须导出var,这对我来说还可以。
Ivaylo Slavov

如果文本文件包含“`”或“。” ,订阅将失败。
shuiqiang

12

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

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

没有其他扩展:名称包含在中的变量引用{...}(例如$varName),命令替代($(...)和传统语法`...`),算术替代($((...))和传统语法$[...])。

要将a $视为文字,请\-e;例如:\${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
}

该函数假定没有0x10x20x3,和0x4控制字符存在于输入,因为那些字符。在内部使用-由于该函数处理text,因此应该是一个安全的假设。


2
这是最好的答案之一。即使使用eval它也是非常安全的。
anubhava

1
此解决方案适用于JSON文件!("正确转义!)
WBAR's

2
此解决方案的一个好处是,它可以让您提供缺少变量的默认值,${FOO:-bar}或者仅在设置为-时输出某些内容${HOME+Home is ${HOME}}。我怀疑有一点扩展,它也可能返回缺少变量的退出代码,${FOO?Foo is missing}但目前没有 tldp.org/LDP/abs/html/parameter-substitution.html列出的列表是否有帮助
Stuart Moore

11

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

2
这去除了双引号字符串
vrtx54234 '18

尝试过:eval“ echo $(cat $ 1)”-不带引号,它对我有用。
access_granted

2
从安全角度来看,这是个坏消息。如果您的模板包含$(rm -rf ~),则您将其作为代码运行。
查尔斯·达菲

eval "echo \"$(cat $1)\"" 很棒!
dev devv

10

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

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

2
这很棒。不一定总是有perl,但是当您这样做时,这很简单明了。
亚伦·麦克米林

5

如果您愿意使用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

1
适用于单个变量,但如何为其他变量包括“或”?
Dana the Sane

2
使用perl可以有多种方法,所有方法都取决于您想要这样做的复杂程度和/或安全性。可以在这里找到更复杂的示例:perlmonks.org/?node_id=718936
Beau Simensen,2009年

3
使用perl比尝试使用shell干净得多。花时间进行这项工作,而不要尝试其他一些提到的基于shell的解决方案。
jdigital

1
最近不得不解决类似的问题。最后,我选择了perl(envsubst看起来有点前途,但很难控制)。
Sfitts

5

这是一种使外壳程序为您进行替换的方法,就像在双引号之间键入文件内容一样。

使用带有内容的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 ~)则模板文件中的文字引号将与您在扩展之前添加的引号相匹配。
查尔斯·达菲

我不是模板内引号与模板外引号匹配,我相信外壳程序会独立地解析模板和终端内字符串(有效地删除引号),然后将它们串联起来。测试的版本,不会删除你的home目录是'$(echo a)'$(echo a)。它产生'a'a。发生的主要事情是对echo a里面的第一个'进行求值,这可能不是您期望的,因为它位于中',但是其行为与包含'在带"引号的字符串中相同。
Apriori

因此,从允许模板作者执行其代码的意义上说,这是不安全的。但是,如何评估报价并不会真正影响安全性。"重点在于扩展任何用引号引起来的字符串(包括$(...))。
Apriori

这是重点吗?我只看到他们要求(而${varname}不是其他)更高安全风险的扩展。
查尔斯·达菲

...也就是说,我必须有所不同(例如:模板内和模板外的引号能够匹配)。当您在字符串中加上单引号时,您将拆分为单引号字符串echo ",然后是带有字面含义的双引号字符串template.txt,然后是另一个文字字符串",所有这些都串联成一个传递给的参数sh -c。您是对的,'不能匹配(因为它是由外壳消耗的而不是传递给内部的),但是可以匹配",因此Gotcha"; rm -rf ~; echo "可以执行包含模板。
查尔斯·达菲,

4

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

2
这是不安全的,因为如果模板中有反斜杠,它将在模板中执行命令替换,例如\$(date)
Peter Dolberg 2016年

1
除了Peter的观点之外,我建议您将其while IFS= read -r line; do用作read命令,否则您将从每条输入行中去除前导和尾随空格。另外,echo可能会将行的开头误认为其命令行选项之一,因此最好使用printf '%s\n'。最后,双引号更安全${1}
mklement0

4

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

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

然后,您可以像下面这样做一个简单的单线:

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

这比eval使用正则表达式或sed


2
好答案!这是一个合适的模板系统,比其他答案更容易使用。
Erfan's

顺便说一句,最好避免cat使用它,<my-file.conf.template而是使用sigil真实的文件句柄而不是FIFO。
查尔斯·达菲

2

我在想同一件事的同时发现了这个线程。这启发了我这一点(注意反引号)

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

4
bash的简写$(cat file)$(< file)
glenn jackman

3
显然,此方法将换行符弄乱了,即,我的文件在一行中全部回显了。
Arthur Corenzan 2014年

@ArthurCorenzan:确实,换行符被空格代替。为了解决这个问题,您必须使用eval echo "\"$(cat FILE)\""它,但是由于输入中的双引号被舍弃了,所以可能仍然不够。
mklement0 2015年

如在其他地方所指出的:仅当您完全信任或控制输入时才使用此命令,因为由于使用,输入中嵌入的命令替换(`…` $(…))允许执行任意命令eval
mklement0 2015年

2

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

use Env;
Env::import();

while(<>) { $_ =~ s/(\${\w+})/$1/eeg; $text .= $_; }

print "$text";

当然,我并不是一个真正的perl人,因此很容易出现致命的缺陷(尽管对我有用)。


1
工作良好。您可以删除该Env::import();行,这意味着use。另外,我建议不要先在内存中建立整个输出:只需在循环内部使用print;而不是$text .= $_;,然后删除循环后print命令即可。
mklement0

1

如果您可以控制配置文件格式,则可以使用bash本身来完成。您只需要来源(“。”)配置文件,而不是对其进行子外壳处理。这样可以确保变量是在当前外壳程序的上下文中创建的(并继续存在),而不是子外壳程序(在子外壳程序退出时变量消失)中。

$ 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和voila中,希望您不会破坏数据库:-)。


1
您不必从config.data文件中导出变量。只需设置它们就足够了。您似乎似乎也没有在任何时候读取模板文件。或者,也许模板文件被修改并包含“ echo”操作...或者我缺少什么?
乔纳森·莱夫勒

1
出口方面的好处是,我默认情况下会这样做,以便子外壳程序可以使用它们,并且不会造成伤害,因为它们会在go出口退出时死亡。“模板”文件是带有回显语句的脚本本身。无需引入第三个文件-基本上是一个mailmerge类型的操作。
paxdiablo

1
“脚本本身及其echo语句”不是模板:而是脚本。考虑<xml type =“ $ TYPE”>和echo'<xml type =“'$ TYPE'”>'之间的可读性(和可维护性)差异
Pierre-Olivier Vares

1
@Pierre,我的配置脚本中没有echo语句,它们只是导出而已,并且我已经展示了如何通过最少的预处理就可以避免这种情况。如果您在其他脚本(例如go.bash)中谈论echo语句,那您将有错的根源-它们不是解决方案的一部分,它们只是表明变量正在被使用的一种方式正确设置。
paxdiablo 2015年

1
@paxdiablo:似乎您只是忘记了一个问题:<<我想将“模板”文件的输出通过管道传递到MySQL >>。因此,使用模板是问题所在,而不是“坚持到底”。导出变量和另一个脚本附和他们只是不回答这个问题在所有
皮埃尔-奥利维尔·瓦雷斯

0

使用备份对可能的多个文件进行perl编辑。

  perl -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : ""/eg' \
    -i.orig \
    -p config/test/*
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.