如果使用Perl是一种选择,并且您只满足于基于环境变量(而不是所有shell变量)的扩展,请考虑Stuart P. Bentley的可靠答案。
该答案旨在提供一种仅使用bash的解决方案,尽管使用了该解决方案,但eval
仍应可以安全使用。
该目标是:
- 支持扩展引用
${name}
和$name
变量引用。
- 防止其他所有扩展:
- 命令替换(
$(...)
和旧式语法`...`
)
- 算术替代(
$((...))
和传统语法$[...]
)。
- 通过以
\
(为前缀,可以有选择地抑制变量扩展\${name}
)。
- 保留特殊字符。在输入中,尤其是
"
和\
实例中。
- 允许通过参数或通过stdin输入。
功能expandVars()
:
expandVars() {
local txtToEval=$* txtToEvalEscaped
# If no arguments were passed, process stdin input.
(( $# == 0 )) && IFS= read -r -d '' txtToEval
# Disable command substitutions and arithmetic expansions to prevent execution
# of arbitrary commands.
# Note that selectively allowing $((...)) or $[...] to enable arithmetic
# expressions is NOT safe, because command substitutions could be embedded in them.
# If you fully trust or control the input, you can remove the `tr` calls below
IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '\1\2\3')
# Pass the string to `eval`, escaping embedded double quotes first.
# `printf %s` ensures that the string is printed without interpretation
# (after processing by by bash).
# The `tr` command reconverts the previously escaped chars. back to their
# literal original.
eval printf %s "\"${txtToEvalEscaped//\"/\\\"}\"" | tr '\1\2\3' '`(['
}
例子:
$ expandVars '\$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls) # only $HOME was expanded
$ printf '\$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
- 出于性能原因,该函数读取标准输入 全部入内存,但是很容易使该函数适应逐行方法。
- 还支持非基本变量扩展,例如
${HOME:0:10}
,只要它们不包含嵌入式命令或算术替换,例如${HOME:0:$(echo 10)}
- 这种嵌入式替换实际上会破坏功能(因为所有
$(
和`
实例都被盲目地转义了)。
- 同样,格式错误的变量引用,例如
${HOME
(缺少关闭}
)会破坏函数。
- 由于bash对双引号字符串的处理,反斜杠的处理方式如下:
\$name
防止扩展。
- 一个
\
不跟单$
保留字符。
- 如果要表示多个相邻
\
实例,则必须将它们加倍;例如:
- 输入不能包含以下(很少使用)字符,其被用于内部用途:
0x1
,0x2
,0x3
。
- 很大程度上存在一个假设性的担忧,即如果bash应该引入新的扩展语法,则此函数可能不会阻止此类扩展-请参见下文,了解不使用的解决方案
eval
。
如果您正在寻找一种仅支持扩展的限制性更强的解决方案${name}
(即使用强制花括号忽略$name
参考),请参阅我的答案。
这是公认的答案的仅bash,eval
-bash解决方案的改进版本:
改进之处包括:
- 支持扩展引用
${name}
和$name
变量引用。
- 支持对
\
不应扩展的变量引用进行转义。
- 与上述
eval
基于解决方案的解决方案不同,
- 非基本扩展被忽略
- 格式错误的变量引用将被忽略(它们不会破坏脚本)
IFS= read -d '' -r lines # read all input from stdin at once
end_offset=${#lines}
while [[ "${lines:0:end_offset}" =~ (.*)\$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
pre=${BASH_REMATCH[1]} # everything before the var. reference
post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
# extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
[[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
# Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
if [[ $pre =~ \\+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
: # no change to $lines, leave escaped var. ref. untouched
else # replace the variable reference with the variable's value using indirect expansion
lines=${pre}${!varName}${post}
fi
end_offset=${#pre}
done
printf %s "$lines"