我有一个用作模板的文本文件,它看起来像这样:
Hostname : $HOSTNAME
Host Address : $HOSTADDRESS
我的 bash 脚本设置两个变量,HOSTNAME
and HOSTADDRESS
,读取模板文件,然后执行 aneval
来扩展$HOSTNAME
and $HOSTADDRESS
:
HOSTNAME="SH_SQL_0089"
HOSTADDRESS="172.16.3.44"
TEMPLATE=`cat template.txt`
MESSAGE=`eval echo $TEMPLATE`
的结果值为MESSAGE
:
Hostname : SH_SQL_0089
Host Address : 172.16.3.44
是否可以将最后两行简化为:
MESSAGE=$(cat template.txt | eval echo ????)
我尝试使用xargs
:
MESSAGE=$( cat template.txt | xargs -i bash -c "eval echo {}" )
但它只是替换$HOSTNAME
并删除了回车符。
我想这样做的原因是我需要添加第三个处理步骤并将eval
'd 输出通过管道传输到该步骤。我感觉我已经很接近了。
答案1
首先,xargs
不能在这里工作,因为替换将在子进程(启动bash
的进程xargs
)中执行。但只有环境变量会传递给子进程,而不是 shell 变量。显然HOSTNAME
在脚本启动时已经存在于您的脚本环境中,但HOSTADDRESS
事实并非如此。此时,您可能会想导出所有变量,但即使这样xargs
也不是一个好的解决方案,因为它会遇到许多引用问题 - 如果您的模板包含\"'
或者您想保留空格,那么您就完蛋了。
现在,看看您当前的代码:除了引用问题之外,MESSAGE=`eval echo $TEMPLATE`
这是一种复杂的编写方式。eval MESSAGE=$TEMPLATE
而且你确实有引用问题;例如,您会注意到所有空白都已折叠。你听说过鲍比桌,不是吗? Shell 扩展规则非常复杂,但有一些规则可以让您保持理智:
"$foo"
变量替换和命令替换始终用双引号引起来"$(bar)"
。如果您了解为什么需要省略引号,则可能会违反此规则。- 使用
$(…)
而不是`…`
用于命令替换。在反引号形式内引用是晦涩难懂且不可移植的,而在内部引用则$(…)
正常。 eval
仅在枪口下按下时使用。如果这样做,请务必小心,并使其尽可能简单。
那么会出现什么问题呢eval MESSAGE="$TEMPLATE"
?它使外壳评估
MESSAGE=Hostname : $HOSTNAME
Host Address : $HOSTADDRESS
糟糕,我们需要在应该是 的值的部分加上引号MESSAGE
。引号必须到达eval
,因此它们需要从字面上经历 shell 扩展的第一阶段:eval MESSAGE="\"$TEMPLATE\""
。
MESSAGE="Hostname : $HOSTNAME
Host Address : $HOSTADDRESS"
更好,但现在您已经有了 Bobby Tables 注入模式 - 如果模板包含引用怎么办?然后你需要转义引号。双引号之间具有特殊含义的四个字符是\"$`
,并且您想$
保留其含义,因此在其他三个字符之前添加反斜杠。
TEMPLATE=$(sed -e 's/[\\"`]/\\&/g' <template.txt)
eval MESSAGE="\"$TEMPLATE\""
现在 shell 将评估
MESSAGE="Hostname : $HOSTNAME
Host Address : $HOSTADDRESS
Name : Bobby \"drop\" O'Tables"
一切都很好。
请注意,此处正确的引用是为了防止意外的解析问题;控制模板的人仍然具有 shell 访问权限(使用$(hello)
)。
如果您希望将模板包含在 shell 脚本中,那么自然可以使用特雷多克。
MESSAGE=$(cat <<EOF)
Hostname : $HOSTNAME
Host Address : $HOSTADDRESS
EOF
但是使用外部模板,您需要执行两步评估,因此使用eval
,并且当涉及到 内部的heredocs 之类的时髦内容时,bash 的解析非常有问题eval
。肯定有一种可行的方法,至少对于 bash 4 来说是这样,但我不建议冒险。
答案2
您希望 shell 应用变量替换,这意味着模板文本必须由 shell 本身作为命令的一部分读取,但模板也是多行文本,因此进入面向行的文本有点棘手通常由 UNIX 命令完成的处理。
一种方法是使用 bash此处文档(在 shell 提示符下键入的所有命令均显示):
创建模板文件:
$ cat template.txt Hi, $NAME Welcome to $PLACE
出口模板文本中应替换的变量:
$ export NAME=Bob $ export PLACE='unix&linux'
创建一个包含单个“换行”
\n
字符的 shell 变量;在 bash 中,最简单的方法是打开一个字符串,输入换行符并关闭它:$ newline=' > '
最后,调用 bash 进行替换:
$ bash -c "cat <<__EOF__${newline}$(cat template.txt)${newline}__EOF__" Hi, Bob Welcome to unix&linux
为什么这有效?让我们从底部解构它:您要求 bash 执行命令(通过选项-c
),但变量替换${newline}
和命令扩展 发生在实际执行$(cat ...)
之前的父 shell 中。bash -c ...
所以结果是bash -c
看到一个嵌入换行符和整个模板文本的命令字符串。就好像您在 bash 提示符下输入了以下内容:
cat <<__EOF__
...contents of template.txt...
__EOF__
另请注意,需要导出替换变量,因为变量是在父 shell 中分配的,但执行替换的是“子”bash。
笔记然而,这种方法很容易导致引用问题:由于模板文本是由 bash shell 解释的,后退学被展开,这可能会导致命令在您的系统上执行。
答案3
eval
为了进行实验,您甚至可以(双倍)嵌入单引号字符串:
export HOSTNAME HOSTADDRESS
(
NL='
'
DELIM=$'\177'
esc="'\''"
export IFS=""
HOSTNAME="SH_SQL_00'8'9"
HOSTADDRESS='172.16.3.44 Bobby "drop" O'\''Tables ${PWD} $(ls)'
printf '%s\n' 'Hostname : $HOSTNAME' 'Host Address : $HOSTADDRESS' > template.txt
TEMPLATE="$(<template.txt)"
TEMPLATE="${TEMPLATE//\'/${esc}}" # escape every single quote: ' --> '\'' (which later will become ''\''' by '${TEMPLATE}')
TEMPLATE="${TEMPLATE//${NL}/${DELIM}}" # replace every newline char with a delim char
evalstr="echo '${TEMPLATE}'"
#printf '\n%s\n\n' "${evalstr}" | LC_ALL=C vis -fotc
set -xv
eval eval "${evalstr}"
) | LC_ALL=C tr '\177' '\n' | nl
答案4
MESSAGE=$(eval $(cat template.txt))