tcler's blog --- 其实我是一个程序员
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.

bash-4.4 new feature ${parameter@operator} ${param@Q}

What

最近浏览 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():

  1. 不需要保护的字符串省略 ‘‘
  2. 像 ${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
}