强制bash在从文件中加载的字符串中展开变量

115
我想知道如何让 Bash 在字符串中(从文件加载)展开变量。我有一个名为 "something.txt" 的文件,其内容如下:
hello $FOO world

我接着运行

export FOO=42
echo $(cat something.txt)
这将返回:
   hello $FOO world

尽管设置了变量,它没有扩展$FOO。我无法评估或源化该文件 - 因为它将尝试执行它(它不可执行 - 我只想要带有插值变量的字符串)。

有什么想法吗?


3
注意eval的安全隐患。参考链接:http://mywiki.wooledge.org/BashFAQ/048。 - Dennis Williamson
您是不是想将该评论留给下面的回答? - Michael Neale
我的意思是对你的评论。有多个答案建议使用eval,了解其潜在影响非常重要。 - Dennis Williamson
@DennisWilliamson 懂了 - 我有点晚回复但是好建议。当然在这些年间我们也有了"壳震荡"之类的东西,所以你的评论经得起时间的考验! 脱帽致敬 - Michael Neale
这个回答解决了你的问题吗?Bash中如何在一个变量中扩展另一个变量 - LoganMzz
14个回答

146

我偶然发现了我认为是这个问题的答案: envsubst 命令:

echo "hello \$FOO world" > source.txt
export FOO=42
envsubst < source.txt

这将输出:hello 42 world

如果您想继续在名为destination.txt的文件中处理数据,请按如下方式将其推回文件:

envsubst < source.txt > destination.txt

如果你的发行版中没有这个软件,它在GNU软件包gettext中。

@Rockallite

  • 我写了一个小包装脚本来解决'$'问题。

(顺便说一句,envsubst有一个“特性”,在https://unix.stackexchange.com/a/294400/7088上有解释,可以只扩展输入中的某些变量,但是我同意转义异常更方便。)

这是我的脚本:

#! /bin/bash
      ## -*-Shell-Script-*-
CmdName=${0##*/}
Usage="usage: $CmdName runs envsubst, but allows '\$' to  keep variables from
    being expanded.
  With option   -sl   '\$' keeps the back-slash.
  Default is to replace  '\$' with '$'
"

if [[ $1 = -h ]]  ;then echo -e >&2  "$Usage" ; exit 1 ;fi
if [[ $1 = -sl ]] ;then  sl='\'  ; shift ;fi

sed 's/\\\$/\${EnVsUbDolR}/g' |  EnVsUbDolR=$sl\$  envsubst  "$@"

7
这是唯一可以保证不执行命令替换、去除引号、处理参数等操作的答案。 - Josh Kelley
6
只有完全导出的 shell 变量才能起作用(在 bash 中)。例如,S='$a $b';a=set;export b=exported;echo $S |envsubst 会得到结果:" exported"。 - gaoithe
1
谢谢 - 我想经过这么多年,我应该接受这个答案了。 - Michael Neale
1
然而,它不处理美元符号($)转义,例如 echo '\$USER' | envsubst 将输出带有前缀反斜杠的当前用户名,而不是 $USER - Rockallite
3
在Mac上使用Homebrew似乎需要brew link --force gettext - KeepCalmAndCarryOn

29

很多使用 evalecho 的答案都有一定的效果,但会因为各种原因而出错,例如多行、尝试转义shell元字符、模板中不打算被bash扩展的转义等。

我也曾遇到同样的问题,并编写了这个Shell函数,据我所知,它可以正确处理所有情况。由于bash的命令替换规则,这个函数仍将仅从模板中去除末尾的换行符,但只要其他所有内容保持完整,我从未发现这是个问题。

apply_shell_expansion() {
    declare file="$1"
    declare data=$(< "$file")
    declare delimiter="__apply_shell_expansion_delimiter__"
    declare command="cat <<$delimiter"$'\n'"$data"$'\n'"$delimiter"
    eval "$command"
}
例如,您可以像这样使用它,使用一个名为parameters.cfg的脚本文件设置变量,以及使用这些变量的模板template.txt
. parameters.cfg
printf "%s\n" "$(apply_shell_expansion template.txt)" > result.txt

在实践中,我将其用作一种轻量级模板系统。


5
这是我迄今为止发现的最好的技巧 :). 根据你的回答,很容易构建一个将变量与包含对其他变量的引用的字符串扩展的函数 - 只需在函数中用$1替换$data即可!顺便说一句,我认为这是对原始问题的最佳答案。 - galaxy
1
即使如此,这也有通常的“eval”陷阱。尝试在输入文件中的任何行末尾添加; $(eval date),它将运行date命令。 - anubhava
3
@anubhava 我不确定你的意思; 在这种情况下,这实际上是期望的扩展行为。换句话说,是的,您应该能够使用此执行任意 shell 代码。 - wjl

29

你可以尝试

echo $(eval echo $(cat something.txt))

12
如果需要保留换行符,请使用 echo -e "$(eval "echo -e \"\<something.txt`"")"`。 - pevik
运行得非常顺畅。 - kyb
2
但是只有当你的 template.txt 是文本时才可以这样做。这个操作很危险,因为它会执行(评估)命令。 - kyb
3
但是这个去掉了双引号。 - Thomas Decaux
归咎于子Shell。 - ingyhere

8

您不想打印每一行,而是想对其进行评估,以便Bash可以执行变量替换。

FOO=42
while read; do
    eval echo "$REPLY"
done < something.txt

请参考help eval或Bash手册获取更多信息。


3
eval 是危险的;你不仅会得到 $varname 的扩展(就像 envsubst 一样),还会得到 $(rm -rf ~) - Charles Duffy

7
另一种方法(看起来有点恶心,但我还是在这里提出了):
将something.txt的内容写入一个临时文件,并用echo语句包装它:
something=$(cat something.txt)

echo "echo \"" > temp.out
echo "$something" >> temp.out
echo "\"" >> temp.out

然后将其源代码返回到一个变量中:

RESULT=$(source temp.out)

使用单行解决方案而不需要临时文件:

并且$RESULT将会展开所有内容。但是这样做似乎不太正确!


只需一行解决方案即可,无需临时文件:

RESULT=$(source <(echo "echo \"$(cat something.txt)\""))
#or
RESULT=$(source <(echo "echo \"$(<something.txt)\""))

1
虽然有点不好看,但它是最直接、简单且有效的。 - kyb
这与其他包含eval的答案一样危险,因为它暗示了隐式的eval。试试echo '$(rm -rf /)' > something.txt,然后再尝试你的解决方案... - undefined

4
如果您只想展开变量引用(这是我自己的目标),您可以执行以下操作。
contents="$(cat something.txt)"
echo $(eval echo \"$contents\")

(在这里)转义$contents周围的引号是关键。

我已经尝试过这个方法,但是在我的情况下$()会删除行末的符号,导致最终的字符串出现错误。 - khusseini
我将 eval 语句改为 eval "echo \"$$contents\"",这样可以保留空格。 - khusseini
@khusseini 我想你的意思是用单个美元符号eval "echo \"$contents\""$$contents是脚本的进程ID,后面跟着单词contents - Soren Bjornstad
这个和其他包含eval的答案一样危险。试试echo '$(rm -rf /)' > something.txt,然后再试试你的解决方案... - undefined

3
  1. If something.txt has only one line, a bash method, (a shorter version of Michael Neale's "icky" answer), using process & command substitution:

    FOO=42 . <(echo -e echo $(<something.txt))
    

    Output:

    hello 42 world
    

    Note that export isn't needed.

  2. If something.txt has one or more lines, a GNU sed evaluate method:

    FOO=42 sed 's/"/\\\"/g;s/.*/echo "&"/e' something.txt
    

1
我发现sed最适合我的目的。我需要从模板生成脚本,这些脚本将从另一个配置文件中导出的变量填充值。它可以正确处理命令扩展的转义,例如在模板中放置\$(date)以获取输出中的$(date)。谢谢! - gadamiak
这和其他包含eval的答案一样危险,因为它暗示了一个隐式的eval。试试echo '$(rm -rf /)' > something.txt,然后再尝试你的解决方案... - undefined

1
foo=45
file=something.txt       # in a file is written: Hello $foo world!
eval echo $(cat $file)

1
感谢您提供这段代码片段,它可能会提供一些有限的、即时的帮助。一个适当的解释将极大地提高其长期价值,因为它可以展示为什么这是一个好的问题解决方案,并使其对未来读者有其他类似问题的人更有用。请[编辑]您的答案以添加一些解释,包括您所做的假设。 - Capricorn
这和其他包含eval的答案一样危险。试试echo '$(rm -rf /)' > something.txt,然后再尝试你的解决方案... - undefined

1

以下是解决方案:

  • 允许替换已定义的变量

  • 保留未定义变量的占位符不变。这在自动化部署中非常有用。

  • 支持以下格式的变量替换:

    ${var_NAME}

    $var_NAME

  • 报告环境中未定义的变量,并为此类情况返回错误代码。



    TARGET_FILE=someFile.txt;
    ERR_CNT=0;

    for VARNAME in $(grep -P -o -e '\$[\{]?(\w+)*[\}]?' ${TARGET_FILE} | sort -u); do     
      VAR_VALUE=${!VARNAME};
      VARNAME2=$(echo $VARNAME| sed -e 's|^\${||g' -e 's|}$||g' -e 's|^\$||g' );
      VAR_VALUE2=${!VARNAME2};

      if [ "xxx" = "xxx$VAR_VALUE2" ]; then
         echo "$VARNAME is undefined ";
         ERR_CNT=$((ERR_CNT+1));
      else
         echo "replacing $VARNAME with $VAR_VALUE2" ;
         sed -i "s|$VARNAME|$VAR_VALUE2|g" ${TARGET_FILE}; 
      fi      
    done

    if [ ${ERR_CNT} -gt 0 ]; then
        echo "Found $ERR_CNT undefined environment variables";
        exit 1 
    fi

0
$ eval echo $(cat something.txt)
hello 42 world
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
Copyright (C) 2007 Free Software Foundation, Inc.

这和其他包含eval的答案一样危险。试试echo '$(rm -rf /)' > something.txt,然后再看你的解决方案…… - undefined

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