bash-4.4 new feature ${parameter@operator} ${param@Q}
07 Feb 2022What
最近浏览 bash man page,看到一个新特性 ${parameter@operator},发现是 bash-4.4 引入的
(RHEL-7 默认 bash-4.2.x, RHEL-8 默认 bash-4.4.x)。其中 ${param@Q} 功能很吸引我,它会自
动给 ‘变量’ 或 ‘数组成员’ 加上引用(但是边界处理有点瑕疵),这也是我一直期待的一个功能,下面
我们来看看效果:
Examples
var
$ var='abc\$ "'
$ echo \$var=$var --- \${var@Q}=${var@Q}
$var=abc\$ " --- ${var@Q}='abc\$ "'
$ var=\''abc\$'\'xyz\'
$ echo \$var=$var --- \${var@Q}=${var@Q}
$var='abc\$'xyz' --- ${var@Q}=''\''abc\$'\''xyz'\''' #前后会多出来两对加引用的空字符 ''
$ var=
$ echo \$var=$var --- \${var@Q}=${var@Q}
$var= --- ${var@Q}=''
$ var=___$'a\nb .*-= z"'\'
$ echo ${var}
___a b .*-= z"'
$ echo "${var}"
___a
b .*-= z"'
$ echo ${var@Q}
$'___a\nb .*-= z"\''
$ echo "${var@Q}"
$'___a\nb .*-= z"\''
array
$ args() { echo "$@"; echo ${@@Q}; echo "${@@Q}"; echo "${*@Q}"; }
$ args === abc "" \' \" \$ ' ' $'a\nb"'\'
=== abc ' " $ a
b"'
'===' 'abc' '' \' '"' '$' ' ' $'a\nb"\''
'===' 'abc' '' \' '"' '$' ' ' $'a\nb"\''
'===' 'abc' '' \' '"' '$' ' ' $'a\nb"\''
Usefulness
首先说我为什么期待这个特性,曾经我需要把命令行参数列表传递给 bash -c “” 来执行,但是 “$*” 会把参数
里的空格或其他特殊字符重新暴露出来,而 “$@” 替换之后是列表,不能直接传给 bash -c ““,所以就需要自己
写 for 循环遍历 “$@” 再给每个参数加上引用,代码如下:
getSafeCommandLine() {
for at; do
if [[ -z "$at" ]]; then
echo -n "'' "
elif [[ "$at" =~ \' ]]; then
echo -n "$at" | sed -r -e ':a;$!{N;ba};' \
-e "s/'+/'\"&\"'/g" -e "s/^/'/" -e "s/$/' /" \
-e "s/^''//" -e "s/'' $/ /"
else
echo -n "'$at' "
fi
done
echo
}
现在有了 ${*@Q} 就不需要再自己写这个特殊函数了,,不过这也就是简化了一点点代码,,要实现透传并执行
整段shell脚本的功能,还是需要像 awk 那样:把整个脚本作为一个单一参数传递,所以我觉得我可能走了弯路,
其实下面这段代码,
getSafeCommandLine() {
#if only one parameter, treat it as a piece of script
[[ $# = 1 ]] && { echo "$1"; return; }
for at; do
if [[ -z "$at" ]]; then
echo -n "'' "
elif [[ "$at" =~ \' ]]; then
echo -n "$at" | sed -r -e ':a;$!{N;ba};' \
-e "s/'+/'\"&\"'/g" -e "s/^/'/" -e "s/$/' /" \
-e "s/^''//" -e "s/'' $/ /"
else
echo -n "'$at' "
fi
done
echo
}
_nsexec() {
local initpid=$1; shift
local _cmdline=$(getSafeCommandLine "$@")
nsenter --target "$initpid" --mount --uts --ipc --net --pid -- bash -c "$_cmdline"
}
_sshexec() {
local sshserv=$1; shift
local _cmdline=$(getSafeCommandLine "$@")
ssh $sshOption root@$sshserv "$_cmdline"
}
只需要简化成下面的样子就可以了(**并不需要给参数加引用,直接运行 $@ 即可 **):
_nsexec() {
local initpid=$1; shift
local EVAL=
[[ $# = 1 ]] && EVAL=eval
nsenter --target "$initpid" --mount --uts --ipc --net --pid -- $EVAL "$@"
}
_sshexec() {
local sshserv=$1; shift
local EVAL=
[[ $# = 1 ]] && EVAL=eval
ssh $sshOption root@$sshserv $EVAL "$@"
}
这里再解释以下上面的 _nsexec() _sshexec 代码,其实就是在特定命名空间、远程主机运行指定的 shell 命令
但是为了敲简单命令的时候更方便(不需要在整个命令行两边加引用),我们判断:
- 如果是参数个数 == 1,就把它当做一段代码用 eval 来执行(或用 bash -c ““也可以)
- 如果是参数个数 > 1,就当作简单命令以 “$@” 方式运行
example:_nsexec $pid 'echo "/expdir (no_root_squash)" >/etc/exports; systemctl restart nfs-server' #复杂命令 _nsexec $pid awk -F: '/myname/ {print $NF}' /etc/passwd #**简单命令(只有命令和参数,没有其他语法关键字)**
Summary
所以 ${@@Q}/${*@Q} 或 getSafeCommandLine() 这个功能,就只剩下在 run(),exec() 里打印更准确的 log 用了,YES!!
run() {
local EVAL= rc=
[[ $# = 1 ]] && EVAL=eval
#echo "[run] $(getSafeCommandLine "$@")" #<<< *** I'm here ***
echo "[run] ${*@Q}" #<<< *** I'm here ***
$EVAL $@
rc=$?
}
***但是还有待改进: 比如${@@Q}把生成的 string 首尾的空 ‘’ 对儿去掉; getSafeCommandLine()已经实现.
REF
how-to-prevent-word-splitting-while-using-eval-to-run-a-command-stored-in
Update (2022-02-14): improve getSafeCommandLine():
- 不需要保护的字符串省略 ‘‘
- 像 ${param@Q} 那样把 含有特殊字符的参数转换成 $’’ 格式 (by using printf %q)
(RHEL-6,RHEL-7,RHEL-8,RHEL-9 测试OK)
getSafeCommandLine() {
#if only one parameter, treat it as a piece of script
[[ $# = 1 ]] && { echo "$1"; return; }
local shpattern='^[][0-9a-zA-Z~@%^_+=:,./-]+$'
for at; do
if [[ -z "$at" ]]; then
echo -n "'' "
elif [[ "$at" =~ $shpattern ]]; then
echo -n "$at "
elif [[ "$at" =~ [^[:print:]]+ ]]; then
echo -n "$(builtin printf %q "$at") "
else
echo -n "$at" | sed -r -e ':a;$!{N;ba};' \
-e "s/'+/'\"&\"'/g" -e "s/^/'/" -e "s/$/' /" \
-e "s/^''//" -e "s/'' $/ /"
fi
done
echo
}