正确引用通过另一个命令间接传递的数组

正确引用通过另一个命令间接传递的数组

我需要将文件名数组传递给命令,并保留正确的引用。到目前为止,一切都很好。不幸的是,该命令实际上是一个子命令,又被另一个命令调用。具体来说,命令是:

git filter-branch --index-filter \
    'git rm -rf --cached ‹file1› ‹file2›…' \
    HEAD

为简单起见,我将在下面用一个显示相同问题的更简单的命令替换它:

printf '%s\n' 'cmd file1 file2…'

现在我有了一个数组files=('a b' c)。我想要的结果是上面的命令打印在一行中,并根据需要单独引用后面的每个标记cmd(例如,当有空格时)。

如果我手动扩展并引用文件名,它会起作用:

$ printf '%s\n' 'cmd '\''a b'\'' c'
→ cmd 'a b' c

(或者,我可以混合使用单引号和双引号来达到相同的结果。)

但如果我尝试传递数组,它就不再起作用:

  1. $ (set -x; printf '%s\n' "cmd '${files[@]}'")
    + printf '%s\n' 'cmd '\''a b' 'c'\'''
    → cmd 'a b
    c'
    
  2. $ (set -x; printf '%s\n' 'cmd '\'"${files[@]}"\')
    + printf '%s\n' 'cmd '\''a b' 'c'\'''
    → cmd 'a b
    c'
    
  3. $ (set -x; printf '%s\n' 'cmd '"${files[@]}")
    + printf '%s\n' 'cmd a b' c
    → cmd a b
    c
    

我对 (3) 不起作用并不感到惊讶(将其包含在内只是为了完整性)。根据 的输出set -x,shell 正确地引用了 (1) 和 (2) 中的各个数组元素,甚至在整个内容周围添加了转义引号。但随后它会分解单独引用的项目。有办法防止这种情况吗?


顺便说一下,Shellcheck (SC2145) 建议将上述[@]部分替换为[*]上述部分。这显然会破坏带有空格的文件名。

答案1

  1. 代替数组,使用set -- file1 file2 ...来填充参数列表,然后使用bash 参数变换Q乌特操作员:

    set -- 'a "b' c "d 'e" "f 'g "'"h' ; (set -x; printf 'cmd %s\n' "${*@Q}")
    

    输出:

    + printf 'cmd %s\n' ''\''a "b'\'' '\''c'\'' '\''d '\''\'\'''\''e'\'' '\''f '\''\'\'''\''g "h'\'''
    cmd 'a "b' 'c' 'd '\''e' 'f '\''g "h'
    

    或者,如果我们删除该set -x; 部分,输出将变为:

    cmd 'a "b' 'c' 'd '\''e' 'f '\''g "h'
    
  2. 来自的评论LL3建议一种更好的方法,不需要 set -- ...

    export x; n=(a "b 'c"); x="${n[@]@Q}"
    ( n=($x); printf 'cmd %s\n' "${n[*]}"; )
    

    简单版本:

    n=(a "b 'c"); echo "cmd ${n[@]@Q}"
    

    输出:

    cmd 'a' 'b '\''c'
    
  3. 另一种方法是使用bash 参数变换A转让运算符(也需要eval):

    export x;n=(a b 'c d');x="${n[@]@A}"; (eval "$x";printf '%s\n' "${n[@]}")
    

    输出显示所printf看到的内容:

    a
    b
    c d
    

答案2

git filter-branch运行/bin/sh /usr/lib/git-core/git-filter-branch该脚本--index-filter评估使用的论点 eval

因此该参数被评估为/bin/sh代码。

在大多数系统上,/bin/sh或多或少是 POSIXsh语言的解释器,尽管在 Solaris 10 及更早版本等少数系统中,它仍然可能是古老的 Bournesh语言。

当谈到引用语法时,它没有什么区别。

无论如何,都$'...'不能使用 ksh/bash/zsh 之类的扩展引用运算符。这意味着您不能使用 GNU/bash/zsh/kshprintf %qmksh/bash ${var@Q}运算符或xtrace跟踪来生成引用,就像$'...'在某些情况下所使用的那样。他们还使用某些不本地化安全的引用形式(例如\)。

您可以使用的一种内置引用运算符是zshqq参数扩展标志,因为它使用单引号:

files=(foo 'a b c' $'a\nb\nc' --foo-- "a'b")
git filter-branch --index-filter "git rm -rf --cached -- ${${(@qq)files}}" HEAD

要查看如何zsh引用这些内容:

$ printf '<%s>\n' "${${(@qq)files}}"
<'foo' 'a b c' 'a
b
c' '--foo--' 'a'\''b'>

使用 bash/ksh/yash/zsh,您可以使用如下函数进行相同的引用:

shquote() {
  LC_ALL=C awk -v q=\' '
    BEGIN{
      for (i=1; i<ARGC; i++) {
        gsub(q, q "\\" q q, ARGV[i])
        printf "%s ", q ARGV[i] q
      }
      print ""
    }' "$@"
}

进而:

git filter-branch --index-filter "git rm -rf --cached -- $(shquote "${files[@]}")" HEAD

答案3

在 Zsh 中,有多种引用选项。最好的是(q+)(q-)中记录的扩展标志zshall(1)。这些添加了更少的不必要的字符:

$ cmd=(ssh localhost "echo hi > t")

$ newcmd=(sh -c "${${(q@)cmd}}"); echo "${${(q@)newcmd}}"
sh -c ssh\ localhost\ echo\\ hi\\ \\>\\ t

$ newcmd=(sh -c "${${(qq@)cmd}}"); echo "${${(qq@)newcmd}}"
'sh' '-c' ''\''ssh'\'' '\''localhost'\'' '\''echo hi > t'\'''

$ newcmd=(sh -c "${${(q-@)cmd}}"); echo "${${(q-@)newcmd}}"
sh -c 'ssh localhost '\''echo hi > t'\'

$ newcmd=(sh -c "${${(qqqq@)cmd}}"); echo "${${(qqqq@)newcmd}}"
$'sh' $'-c' $'$\'ssh\' $\'localhost\' $\'echo hi > t\''

至于 的语法"${${(q@)cmd}}"q(或qqq-)会导致应用转义或引用。这@会导致此转义应用于数组的每个元素cmd。外部${...}似乎相当于${(j: :)...},即用空格连接。需要双引号,以便结果不会再次拆分。

不幸的是,Zsh 和 Bash 中的所有引用机制对于某些输入的引用深度都是指数级的。

以下示例显示了各种报价扩展运算符的增长率(代码如下):

q: (1) 6; (2) 14; (3) 24; (4) 42; (5) 76; (6) 142; (7) 272; (8) 530; 
qq: (1) 5; (2) 15; (3) 43; (4) 125; (5) 369; (6) 1099; (7) 3287; (8) 9849; 
qqq: (1) 5; (2) 13; (3) 25; (4) 45; (5) 81; (6) 149; (7) 281; (8) 541; 
qqqq: (1) 6; (2) 14; (3) 24; (4) 39; (5) 64; (6) 109; (7) 194; (8) 359; 
q-: (1) 5; (2) 15; (3) 39; (4) 97; (5) 237; (6) 575; (7) 1391; (8) 3361; 
q+: (1) 6; (2) 16; (3) 40; (4) 98; (5) 238; (6) 576; (7) 1392; (8) 3362; 

奇怪的是,qqqq它的生长速度最慢,尽管它直到第四层筑巢才开始落后。

Tcl 是一种很棒的语言,它具有具有线性增长属性的嵌套引用运算符(请参阅下面的第 6 项man tcl)。

这是实验的代码。我用作$'\t'初始字符串,因为它为q+和提供了不同的长度q-

f (){
  flag=$1
  echo -n "$flag: "
  str=$'\t'
  for i in $(seq 1 10); do
    eval 'str=\"${${('$flag'@)str}}\"'
    N=$(echo -n $str | wc -c)
    echo -n "($i) $N; "
  done
  echo
}
f q
f qq
f qqq
f qqqq
f q-
f q+

答案4

$ foo=(1 2 '3 4' 4 5)
$ printf "'%s'\n" "${foo[@]}"
'1'
'2'
'3 4'
'4'
'5'
$ subcommand() { printf "'%s'\n" "$@"; }
$ subcommand "${foo[@]}"
'1'
'2'
'3 4'
'4'
'5'

因此,让我们根据您的具体用例进行调整:

git filter-branch --index-filter \
    'git rm -rf --cached file1 file2 […]' \
    HEAD

对于你的情况,我们需要更有创意一点,并将事情分解成更小的部分。

git filter-branch --index-filter  \
    'git rm -rf --cached [MAGIC]' \
    HEAD

我们正在创建的文件列表是您需要“魔法”发生的地方。其余的都是静态的,是吗?既然你正在编写这个脚本,你就不需要需要它分为三行,这简化了事情:

git filter-branch --index-filter 'git rm -rf --cached [MAGIC]' HEAD

所以:

prefix="git filter-branch --index-filter 'git rm -rf --cached "
postfix="' HEAD"
magic="$(printf '"%s" ' "${file[@]}"'

然后如果我们执行:

${prefix}${magic}${postfix}

因此,我们已经组装了您的命令 - 尽管使用"s 而不是's 来括起您的文件名,因为该filter-branch命令已经在's 中。

相关内容