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

231
我想将一个包含像${dbName}这样的变量的“模板”文件的输出导入到MySQL中。有什么命令行工具可以替换这些实例并将输出转储到标准输出?输入文件被认为是安全的,但可能存在错误的替换定义。执行替换应避免执行意外的代码执行。
19个回答

275
更新:尝试使用envsubst
这里有一个类似问题的解决方案,来自yottatsa,它只替换像$VAR或${VAR}这样的变量,并且是一个简洁的一行代码。
i=32 word=foo envsubst < template.txt

当然,如果iword在你的环境中,那就是这样的。
envsubst < template.txt

在我的Mac上,它看起来是作为gettextMacGPG2的一部分安装的。

旧答案:尝试使用eval

这是对mogsie在类似问题上提出的解决方案的改进,我的解决方案不需要你转义双引号,而mogsie的解决方案是一行代码!

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

这两个解决方案的优势在于,你只会遇到几种不常见的 shell 扩展,即 $((...))、`...` 和 $(...),尽管反斜杠在这里是一个转义字符,但你不必担心解析过程中出现错误,而且它也可以处理多行文本。

10
如果您的环境变量没有被导出,裸露的envsubst无法正常工作。 - Toddius Zho
9
@ToddiusZho说:不存在未导出的环境变量——正是通过导出,才使一个“shell”变量成为环境变量。“envsubst”正如其名称所示,只识别环境变量而不是shell变量。另外值得注意的是,“envsubst”是一个“GNU”实用程序,因此并非预安装或在所有平台上都可用。 - mklement0
3
也许另一种说法是,envsubst 只能看到自己的进程环境变量,因此您可能在之前(在单独的行上)定义的“普通” shell 变量,除非您“导出”它们,否则不会被子进程继承。在我上面 gettext 的示例用法中,我通过一个 bash 机制修改了继承的 gettext 环境,通过在要运行的命令前缀中添加它们。 - plockc
1
我有一个字符串包含了$HOME,我发现$HOME作为默认shell运行时可以工作,但是我想要它作为我的自定义/home/zw963来使用,然而似乎不支持$(cat /etc/hostname)的替代,所以它并不能完全满足我的需求。 - zw963
4
谢谢您的“旧回答”,因为它不仅允许使用变量,还可以像$(ls -l)这样使用shell命令。 - Alek
显示剩余5条评论

245

Sed

给定template.txt:

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

我们只需要说:

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

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


18
你可以将这两个sed命令合并为一个:sed -e "s/${i}/1/" -e "s/${word}/dog/"; 这样更有效率。如果进行大约100次此类操作可能会遇到问题(这是多年前的问题,可能不再适用,但请注意HP-UX)。 - Jonathan Leffler
3
小提示:如果在给定的示例中,“1”或“dog”包含美元符号,则需要使用反斜杠进行转义(否则不会进行替换)。 - MatthieuP
9
你不需要使用 cat 命令。你只需要运行以下命令:sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text,它会将模板文件中的 ${i} 替换为 1${word} 替换为 dog - HardlyKnowEm
4
如果替换文本是密码,那么sed会期望一个已转义的文本,这很麻烦。 - jpbochi
4
要将结果写入文本文件,您可以使用 sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text | tee newFile - rubiktubik
显示剩余5条评论

52

请使用/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
为什么不直接使用以下命令:while read line ; do eval echo "$line"; done < ./template.txt ?这样就不需要将整个文件读入内存,然后通过大量使用head和tail逐行输出。但是'eval'是可以的——除非模板包含像反引号之类的shell字符。 - Jonathan Leffler
26
非常危险!输入中的所有“bash”命令都将被执行。如果模板是:“这是一个词语; rm -rf $HOME”,您将会丢失文件。 - rzymek
1
@rzymek - 记住,他想直接将这个文件导入数据库。因此显然输入是可信的。 - gnud
4
信任一个文件存储其内容和信任它能够执行其中包含的任何内容是有区别的。 - Mark
3
请注意以下限制:(a)输入中的双引号将被静默丢弃,(b)read 命令会修剪每行开头和结尾的空格并“吃掉”\ 字符,(c)只有在完全信任或控制输入时才使用此功能,因为嵌入在输入中的命令替换(\...` $(...))允许由于使用 eval 执行任意命令。最后,echo` 有很小的机会将一行的开头误认为是其命令行选项之一。 - mklement0
1
如果使用括号会出现错误。例如:"db.enableSharding(${db})" 无法正常工作。 - JohnC

24

最近因为大家的兴趣,我又再次考虑了这个问题,并且我认为我最初想到的工具是m4,这是用于Autotools的宏处理器。因此,您可以使用我最初指定的变量来代替:

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

1
这个解决方案是这里答案中缺点最少的。你知道有没有办法替换${DBNAME}而不仅仅是DBNAME? - Jack Davidson
@JackDavidson 对于这种简单的变量替换/模板使用,我会像其他答案中提到的那样使用envsubstm4是一个很棒的工具,但它是一个完整的预处理器,具有更多功能和复杂性,如果您只想替换一些变量,则可能不需要它。 - imiric

22

创建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
尝试过:eval "echo $(cat $1)" - 没有引号,对我来说可行。 - access_granted
5
从安全角度来看,这是个坏消息。如果你的模板包含 $(rm -rf ~),那么你就会将其作为代码运行。 - Charles Duffy
eval "echo \"$(cat $1)\"" 运行良好! - dev devv

16

template.txt

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

data.sh

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

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

2
正如其他地方所指出的一样:仅在您完全信任或控制输入时使用此功能,因为嵌入在输入中的命令替换(\…`或$(...))会由于使用eval而允许执行任意命令,并且由于使用source而直接执行Shell代码。 此外,输入中的双引号会被静默丢弃,而echo可能会将一行开头误认为是其命令行选项之一。 - mklement0
不幸的是,这会从结果文件中删除所有双引号(")。有没有一种方法可以在不删除双引号的情况下完成相同的操作? - Ivaylo Slavov
我在这里找到了我要找的东西:https://dev59.com/7XE85IYBdhLWcg3wKwKD#11050943;我使用了envsubst。不同之处在于变量必须被导出,这对我来说没问题。 - Ivaylo Slavov
如果文本文件包含 "`" 或 ".",替换将失败。 - shuiqiang

13
这是一个强大的Bash函数,尽管使用了eval,但仍应安全使用。输入文本中的所有${varName}变量引用都基于调用shell的变量进行扩展。
没有扩展其他内容:既不是未用{}括起来的变量引用(例如$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控制字符,因为这些字符在内部使用 - 由于该函数处理的是文本,所以这应该是一个安全的假设。


2
这是这里最好的答案之一。即使使用eval,它也非常安全可靠。 - anubhava
1
此解决方案适用于 JSON 文件!(正确转义 " !) - WBAR
2
这个解决方案的好处是它允许您为缺失的变量${FOO:-bar}提供默认值,或者仅在设置了某些内容时输出${HOME+Home is ${HOME}}。我猜测通过一点扩展,它还可以返回缺失变量${FOO?Foo is missing}的退出代码,但目前尚未实现。如果需要,https://www.tldp.org/LDP/abs/html/parameter-substitution.html上有这些内容的列表。 - Stuart Moore
1
最佳答案在这里。所有的 " 和 ' 都已经完全转义。仅使用 eval 的解决方案对带有 ' 或 " 的文件不起作用。 - jmcollin92

13

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

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

2
这很棒。并不总是有perl,但当你有时,这很简单和直接。 - Aaron McMillin

7
我建议使用类似于Sigil的工具: https://github.com/gliderlabs/sigil 它被编译为单个二进制文件,因此在系统上安装非常容易。
然后您可以执行以下简单的一行命令:
cat my-file.conf.template | sigil -p $(env) > my-file.conf

这比使用eval更安全,比使用正则表达式或sed更容易。


2
很棒的答案!这是一个合适的模板系统,比其他答案更容易使用。 - Erfan
1
顺便提一下,最好避免使用 cat 命令,而是使用 <my-file.conf.template 命令,这样可以给 sigil 一个真正的文件句柄,而不是一个 FIFO。 - Charles Duffy

6

以下是一种让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 是指定的环境变量。

1
从安全角度来看,这是个坏消息。如果你的模板包含 '$(rm -rf ~)'$(rm -rf ~) 这样的内容,在模板文件中的字面引号将与它扩展前添加的引号匹配。 - Charles Duffy
这是重点吗?我只看到他们要求${varname},而不是其他更高安全风险的扩展。 - Charles Duffy
公正的观点;因此,文件不能包含文字",否则它将与shell中的"匹配。您可以传递'echo "'"$(sed 's/"/\\"/g' template)"'"'而不是'echo "'"$(cat template.txt)"'"'到sh -c来解决这个问题(但现在我们又回到了正则表达式的hack)。我需要再考虑一下,但可能仍然有方法欺骗它做一些不直观的事情。 - Apriori
也许扩展$(...)不是原问题的重点;相反,当我发现这个问题时,我正在寻找一个解决方案,然后提供了一个答案。 老实说,我对代码执行部分也没有用处,但是用正则表达式替换变量很糟糕,并且在其他方面不会像shell变量那样运行,因此我认为让shell执行插值的一行代码非常优雅。不幸的是,我不知道调用sh以使其扩展变量但不执行代码的方法。 - Apriori
可以将 env -i 作为前缀,以切断外部环境。 - plockc
显示剩余6条评论

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