我需要将文件名数组传递给命令,并保留正确的引用。到目前为止,一切都很好。不幸的是,该命令实际上是一个子命令,又被另一个命令调用。具体来说,命令是:
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
(或者,我可以混合使用单引号和双引号来达到相同的结果。)
但如果我尝试传递数组,它就不再起作用:
$ (set -x; printf '%s\n' "cmd '${files[@]}'") + printf '%s\n' 'cmd '\''a b' 'c'\''' → cmd 'a b c'
$ (set -x; printf '%s\n' 'cmd '\'"${files[@]}"\') + printf '%s\n' 'cmd '\''a b' 'c'\''' → cmd 'a b c'
$ (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
代替数组,使用
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'
来自的评论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'
另一种方法是使用
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 %q
或mksh/bash
${var@Q}
运算符或xtrace
跟踪来生成引用,就像$'...'
在某些情况下所使用的那样。他们还使用某些不本地化安全的引用形式(例如\
)。
您可以使用的一种内置引用运算符是zsh
的qq
参数扩展标志,因为它使用单引号:
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
(或qq
等q-
)会导致应用转义或引用。这@
会导致此转义应用于数组的每个元素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 中。