引号在反引号输出中的处理方式不同

引号在反引号输出中的处理方式不同

背景

我想将find包含空格的文件名列表(通过 列出)传递给我的自定义 python 脚本。因此,我设置find在每个结果周围添加引号:

find ./testdata -type f -printf "\"%p\" "

结果:

"./testdata/export (1).csv" "./testdata/export (2).csv" "./testdata/export (3).csv"

为了回答这个问题,我们假设我的自定义脚本 ( test.py) 执行以下操作:

#!/usr/bin/python3
import sys 


print(sys.argv)

观察结果

情况1:

手动列出引用的参数。

输入:./test.py "./testdata/export (1).csv" "./testdata/export (2).csv" "./testdata/export (3).csv"

输出:['./test.py', './testdata/export (1).csv', './testdata/export (2).csv', './testdata/export (3).csv']

案例2:

使用xargs

输入:find ./testdata -type f -printf "\"%p\" " | xargs ./test.py

输出:['./test.py', './testdata/export (1).csv', './testdata/export (2).csv', './testdata/export (3).csv']

(即,输出与情况1

案例3:

使用反引号。

输入:./test.py `find ./testdata -type f -printf "\"%p\" "`

输出:['./test.py', '"./testdata/export', '(1).csv"', '"./testdata/export', '(2).csv"', '"./testdata/export', '(3).csv"']

有两件事发生了变化:

  • "./testdata/export(1).csv"现在是两个单独的参数。
  • 引用仍然是论点的一部分

问题

  1. 为什么带反引号的版本表现不同?

  2. 有没有办法仍然包含带有反引号的引号?即,让它们的行为与xargs?相同。

评论

我实在无法想象这里发生了什么。一种合乎逻辑的解释可能是,反引号中的命令输出将被视为一个大参数。但是,为什么它会在空白处分裂呢?

因此,下一个最好的解释似乎是每个空格分隔的字符串都被视为一个单独的参数,而不考虑引用。它是否正确?如果是这样,为什么反引号有这种奇怪的行为?我想这不是我们大多数时候想要的......

答案1

因此,下一个最好的解释似乎是每个空格分隔的字符串都被视为一个单独的参数,而不考虑引用。它是否正确?

是的,参见例如https://mywiki.wooledge.org/WordSplitting为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?什么时候需要双引号?

shell 仅当引号最初位于命令行上时才处理引号,而不是任何扩展的结果(例如您在此处使用的命令替换或参数扩展),并且引号本身不被引用。

如果是这样,为什么反引号有这种奇怪的行为?我想这不是我们大多数时候想要的......

嗯,陌生感是相对的。在一种情况下,一个人想要的东西可能根本不是在另一种情况下任何人想要的东西。

但请考虑这样的事情:

a="blah blah"
somecmd -f "$a"

它的工作方式是somecmd获取变量中包含的字符串作为参数a不管它包含什么。这与“真实”编程语言(例如 Python)中的工作方式类似subprocess.call(["somecmd", "-f", a])。简单、干净且完全安全:变量中没有特殊字符可能会造成混乱。

如果字符串来自脚本外部、从文件读取、由用户输入或作为文件名扩展的结果,这一点很重要。

echo "Please enter a filename: "
read -r a
somecmd -f "$a"

如果扩展结果是针对引号进行处理的,则您无法输入Don't stop me now.mp3文件名,因为存在不成对的引号。

另外,是否也应该处理所有扩展的结果以进行进一步的扩展?设置a$(rm -rf $HOME).txt会做一些相当令人讨厌的事情。请注意,这是一个完全有效的文件名,因此它可以作为像*.txt.

我知道,这有点夸张,因为我们可以建议在扩展后只处理引号和转义符,而不是任何进一步的扩展。不成对的单引号仍然是一个问题,并且$(find -printf "\"%p\"")对于包含双引号的文件名仍然不起作用。

也许类似的东西可以发挥作用,但是魔法处理越不安静,发生事故的可能性就越小。(对于外壳,我有时认为我们应该很高兴它是如此理智。)


但你是对的,这意味着没有立即明显的直接方法可以将字符串列表从findshell 中获取。这实际上是您真正想要的,一个字符串列表,就像sys.argv在 Python 中一样。不是引号。

您可以执行以下操作:

find -print0 | xargs -0 ./test.py

-print0要求find打印以 NUL 字节作为分隔符(而不是换行符)的文件名,并-0告诉xargs我们只需要这样。这是可行的,因为 NUL 字节是唯一不能包含在文件名中的内容。至少在 GNU 和 FreeBSD 中都可以找到-print0-0

或者,在 Bash 中:

mapfile -d '' files < <(find -print0)
./test.py "${files[@]}"

这与用于进程替换和数组的 NUL 分隔字符串相同。

或者,在 Bash (带有shopt -s globstar) 和其他具有类似功能的程序中,如果您不需要基于文件名以外的任何内容进行过滤:

shopt -s globstar
./test.py ./testdata/**

**就像*,只是递归。

或者,使用标准工具:

find -exec ./test.py {} +

find这通过要求运行自身来绕过整个问题test.py,而不将文件名列表传递到其他任何地方。不过,如果您确实需要将列表存储在某处,则没有帮助。请注意+最后的,将为每个文件-exec ./test.py {} \;运行一次。test.py

答案2

xargs对输入进行自己的特殊处理。

它将所有换行符和空格序列(至少是空格和制表符,在某些实现中更多)视为分隔符,忽略前导和尾随序列,并以自己的特殊方式处理引用:'...'"..."并且\可以用于引用,但方式与语法的sh( 和"..."都是'...'强引号,但不能包含换行符,并且\newline是文字换行符而不是续行符)。

所以对于这样的输入:

   "foo \ bar" 'x'\
y

xargs生成两个foo \ barx<newline>y参数。

split+glob 运算符在 POSIX shell 的列表上下文中保留不带引号的命令替换(古代形式`...`和现代形式)。使用复杂的规则$(...)将输入分割为$IFS字符,并且生成的单词受到文件名生成。根本没有报价处理。

在像这样的输入上

  "a* b"

使用默认值$IFS(SPC,TAB,NL),它会生成一个单词,该单词进一步扩展为当前目录中以和"a*开头的文件名列表。"ab"

命令行如下:

cmd "a* b"
cmd2 "x\"y"

是 shell 语法中的代码。在 shell 的语法中,空格、换行符和引号也具有特殊含义,并且对于 的解释也不同xargs。上面的代码被解析为两个命令,因为换行符分隔命令,cmd "a* b"被解析为两个单词:cmd并且a* b作为空格分隔单词,并且"..."是一个 shell 引用运算符,可防止*其中的 和 SPC 被特殊处理...等等。

要以与 shell 相同的方式进行标记化,zsh有一个zglob 限定符(请注意,默认情况下 zsh 不是 POSIX,因为它仅在列表上下文中的无引号命令替换时进行 split 而不是 split+glob),并且还有一个Qglob限定符删除一层引用。在该 shell 中你可以执行以下操作:

output_of_cmd=$(find...) # no split+glob here as we're assigning to
                         # scalar variable. It's not a list context

words=("${(Q@)${(z)output_of_cmd}}") # array assignment
your-app "${words[@]}"

答案3

由于命令替换中的 shell 扩展,您丢失了引号,您只需要再次引用它即可。建议您使用该$()形式而不是反引号。它使您的代码更具可读性。

eval ./test.py "$(find ./testdata -type f  -printf "\"%p\" ")"

更新:为了像现在的其他示例一样,我将 eval 放在前面,这将导致正确的扩展/引用,以便您获得 python 的单独引用参数。

相关内容