ChatGPT解决这个技术问题 Extra ChatGPT

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

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


C
Community

更新

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

i=32 word=foo envsubst < template.txt

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

envsubst < template.txt

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

旧答案

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

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

这两种解决方案的强大之处在于,您只会得到几种通常不会发生的 shell 扩展 $((...))、`...` 和 $(...),尽管反斜杠是此处转义字符,但您不必担心解析有错误,并且它可以很好地执行多行。


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

Sed

给定模板.txt:

The number is ${i}
The word is ${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/";那更有效。某些版本的 sed 可能会在 100 次这样的操作时遇到问题(几年前的问题 - 可能仍然不正确,但要注意 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
P
Peter Mortensen

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

文件模板.txt:

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"

输出:

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

为什么不只是: while read line ;执行 eval echo "$line";完成 < ./template.txt ???无需将整个文件读入内存,只需通过大量使用 head 和 tail 一次将其吐出一行。但是 'eval' 是可以的 - 除非模板包含像反引号这样的 shell 字符。
这是非常危险的!将执行输入中的所有 bash 命令。如果模板是:“单词是;rm -rf $HOME”,您将丢失文件。
@rzymek - 请记住,他想将此文件直接通过管道传输到数据库。如此看来,输入是可信的。
@gnud 足够信任文件以存储其内容与足够信任文件以执行其包含的任何内容之间存在差异。
注意约束:(a)输入中的双引号被悄悄地丢弃,(b)read命令,如所写,修剪每行的前导和尾随空格并“吃掉”\ 个字符。,(c)仅当您完全信任或控制输入时才使用此选项,因为嵌入在输入中的命令替换(`…` $(…))允许由于使用 eval 而执行任意命令。最后,echo 将一行的开头误认为是它的命令行选项之一的可能性很小。
D
Dana the Sane

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

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

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

创建 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)\"" 效果很好!
C
ChaPuZ

模板.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。不同之处在于必须导出变量,这对我来说没问题。
如果文本文件包含“`”或“.” ,替代将失败。
m
mklement0

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

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

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

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

请注意,仅通过标准输入接受输入。

例子:

$ 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 目前没有这些列表
最佳答案在这里。所有 " 和 ' 都被完全转义。只有 eval 的解决方案不适用于带有 ' 或 " 的文件
T
Thomas

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

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

这很棒。不要总是有 perl,但是当你这样做时,这很简单直接。
s
spudfkc

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

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

然后你可以做一个简单的单行,如下所示:

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

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


很好的答案!这是一个合适的模板系统,比其他答案更容易使用。
顺便说一句,最好避免使用 cat 并改用 <my-file.conf.template,这样您就可以给 sigil 一个真正的文件句柄而不是 FIFO。
A
Apriori

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

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

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

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

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

解释:

i 和 word 作为环境变量传递给 sh 的执行。

sh 执行它传递的字符串的内容。

一个接一个地写成一个字符串,那个字符串是:'echo "' + "$(cat template.txt)" + '"'

'echo "' + "$(cat template.txt)" + '"'

由于替换是在 ", "$(cat template.txt)" 之间变成了 cat template.txt 的输出。

所以 sh -c 执行的命令变成:echo "The number is ${i}\nThe word is ${word}",其中i和word是指定的环境变量。

echo "数字是${i}\n单词是${word}",

其中 i 和 word 是指定的环境变量。


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

如果您愿意使用 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 看起来很有希望,但它太难控制了)。
u
user976433

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

脚本.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'。最后,双引号 $while IFS= read -r line; do 更安全。
g
glenn jackman

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

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

$(cat file) 的 bash 简写是 $(< file)
显然这种方法弄乱了换行符,即我的文件在一行中得到了全部回显。
@ArthurCorenzan:确实,换行符已替换为空格。要解决此问题,您必须使用 eval echo "\"$(cat FILE)\"" 但这可能仍然不足,因为输入中的双引号被丢弃。
正如其他地方所指出的:仅当您完全信任或控制输入时才使用此选项,因为输入中嵌入的命令替换(`…` $(…))允许执行任意命令,因为使用 eval
保留换行符和引号:stackoverflow.com/a/17030906/10390714
s
sfitts

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

use Env;
Env::import();

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

print "$text";

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


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

如果您可以控制配置文件格式,则可以在 bash 本身中完成。您只需要 source (".") 配置文件而不是 subshell。这确保了在当前 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 '' 之间的可读性(和可维护性)差异
@Pierre,我的配置脚本中没有 echo 语句,它们只是导出,我已经展示了如何通过最少的预处理来避免这种情况。如果您在谈论我的其他脚本(例如 go.bash)中的 echo 语句,那么您就大错特错了 - 它们不是解决方案的一部分,它们只是一种表明变量设置正确。
@paxdiablo:看来您只是忘记了这个问题:<<我想将“模板”文件的输出通过管道传输到 MySQL >>。所以使用模板是个问题,它不是“大错特错”。导出变量并在另一个脚本中回显它们根本不能回答问题
j
joehep

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

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

o
olopopo

我创建了一个名为 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 的更多信息


r
roy man

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

嵌套模板示例

创建模板文件,变量采用常规 bash 语法 ${VARIABLE_NAME} 或 $VARIABLE_NAME

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

template.txt

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

nested-template.txt

Nice to have you here ${name} :\)

创建源文件

template.source

declare name=royman 

解析模板

source template.source && eval echo "$(<template.txt)"

输出

Hello royman!
Nice to have you here royman :)