使用生成的文件名列表作为参数列表-带空格


16

我正在尝试使用收集的文件名列表调用脚本find。没什么特别的,只是这样:

$ myscript `find . -name something.txt`

问题在于某些路径名包含空格,因此在参数扩展时它们会分解为两个无效的名称。通常,我会在名称两边加上引号,但是在这里,它们会由反引号扩展插入。我试过find用引号过滤每个文件名的输出并用引号引起来,但是当bash看到它们时,剥离它们已经为时已晚,它们被视为文件名的一部分:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

是的,这是处理命令行的规则,但是如何解决呢?

这很尴尬,但我没有提出正确的方法。我终于想出了解决方法,但是xargs -0 -n 10000...我仍然想问这个丑陋的hack:我该如何引用反引号扩展的结果,或者以其他方式实现相同的效果?

编辑:我对xargs 确实将所有参数收集到一个参数列表中的事实感到困惑,除非被告知否则可能会超出系统限制。感谢大家使我挺直!其他人,在阅读已接受的答案时,请记住这一点,因为它不是很直接地指出。

我已经接受了答案,但我的问题仍然存在:是否存在某种方法来保护反引号(或$(...))扩展中的空间?(请注意,可接受的解决方案是非强制性的答案)。


我猜您需要更改shell用作文件名分隔符的内容(例如,通过使用IFS的值,一种可能的方式是IFS=",newline,")。但是是否需要对所有文件名执行脚本?如果不是,请考虑使用find本身为每个文件执行脚本。
njsg 2014年

更改IFS是一个好主意,没想到!对于命令行用法不切实际,但仍然如此。:-)是的,目标是将所有参数传递给脚本的同一调用。
亚历克西斯

Answers:


12

您可以使用findxargs这样的一些实现来执行以下操作。

$ find . -type f -print0 | xargs -r0 ./myscript

或者,通常,只是find

$ find . -type f -exec ./myscript {} +

说我有以下示例目录。

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

现在假设我有这个./myscript

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

现在,当我运行以下命令时。

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

或者当我使用第二种形式时:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

细节

查找+ xargs

上面两种方法虽然看起来有所不同,但本质上是相同的。第一种是从find中获取输出,\0通过-print0开关使用NULLs()将其拆分。该xargs -0专门采取输入这是使用NULL的分裂。该非标准语法是GNU引入的findxargs但如今在其他一些语言(如最新的BSD)中也可以找到。如果在GNU中找不到任何东西,而在BSD中却找不到任何东西,-r则需要使用该选项以避免调用。myscriptfindfind

注意:整个方法取决于您永远不会传递太长的字符串的事实。如果是,则将./myscript启动find 的第二次调用,并从其余的后续结果中开始。

用+查找

这是标准方式(尽管它只是在最近才(2005)才添加到的GNU实现中find)。xargs字面上内置了执行我们正在做的事情的能力find。因此,find将找到文件列表,然后将该列表传递尽可能多的参数,以适应之后指定的命令-exec(请注意,在这种情况下{}只能是最后一个+),并在需要时多次运行命令。

为什么不报价?

在第一个示例中,我们通过使用NULL分隔参数来完全避免引用问题,从而采取了捷径。当xargs给出此列表时,它被指示拆分为NULL,以有效保护我们的单个命令原子。

在第二个示例中,我们将结果保留在内部find,因此它知道每个文件原子是什么,并将保证适当地处理它们,从而避免了引用这些文件的麻烦。

命令行的最大大小?

这个问题会不时出现,因此,作为奖励,我将其添加到此答案中,主要是为了将来我能找到它。您可以xargs用来查看环境的限制,如下所示:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072

1
谢谢,但是我需要将所有参数传递给脚本的相同调用。那是在问题描述中,但是我想我并没有说清楚这不是偶然的。
亚历克西斯

@alexis-再次阅读答案,它们将所有参数传递给脚本的单个调用。
slm

我完蛋了!我不知道关于(我也使用散文的+说法,所以我第一次错过了你的解释)。但更重要的是,我会误解默认情况下的功能!!!在使用Unix的三十年中,到目前为止,我从未使用过它,但我以为我知道我的工具箱...find+xargs
Alexis 2014年

@alexis-我认为您错过了我们在说的话。是xargs的,是命令的魔鬼。您必须阅读它和find的手册页很多次才能了解它们可以做什么。开关的可能彼此相反,因此增加了混乱。
slm

@alexis-还要添加到工具箱中的一件事,不要使用反引号/反引号来运行嵌套命令,请立即使用$(..)。它会自动处理引号等的嵌套。不建议使用反引号。
slm

3
find . -name something.txt -exec myscript {} +

在上面, find找到所有匹配的文件名,并将它们作为的参数提供myscript。这适用于文件名,而不管空格或任何其他奇数字符。

如果所有文件名都适合一行,则myscript将执行一次。如果列表太长而无法处理Shell,则find将根据需要多次运行myscript。

更多:命令行上可以容纳多少个文件? man findfind建立命令行“与xargs建立命令行的方式几乎相同”。并且,man xargs限制取决于系统,您可以通过运行来确定限制 xargs --show-limits。(getconf ARG_MAX也是一种可能)。在Linux上,每个命令行的限制通常(但并非总是)约为200万个字符。


2

除了@slm的很好的答案。

参数大小的限制是在execve(2)系统调用上(实际上,是参数,环境字符串和指针的累积大小)。如果myscript是以外壳程序可以解释的语言编写的,那么也许您不需要执行它,则可以让外壳程序仅解释它而不必执行另一个解释器。

如果您以以下方式运行脚本:

(. myscript x y)

就像是:

myscript x y

除了当前shell的子进程正在解释它,而不是执行它(最终涉及执行 sh使用甚至更多参数(或she-bang行指定的任何内容))。

现在显然,您不能再使用find -exec {} +.命令了,因为.它是Shell的内置命令,它必须由Shell执行,而不是find

使用zsh,很容易:

IFS=$'\0'
(. myscript $(find ... -print0))

要么:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

尽管有了zsh,您首先不需要find,因为它的大多数功能都内置于zshglob中。

bash但是变量不能包含NUL字符,因此您必须找到另一种方法。一种方法是:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

您可能还会globstarbash4.0及更高版本中将zsh样式递归glob与option选项一起使用:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

请注意,**符号链接跟随目录,直到它在bash4.3 中修复。还要注意,bash它没有实现zsh通配符限定符,因此您不会在find那里获得所有功能。

另一种选择是使用GNU ls

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

如果要确保myscript执行一次(如果参数列表太大,则失败),也可以使用上述方法。在最新版本的Linux上,您可以使用以下方法提高甚至取消对参数列表的限制:

ulimit -s 1048576

(1GiB堆栈大小,其中四分之一可用于arg + env列表)。

ulimit -s unlimited

(没有限制)


1

在大多数系统中,使用xargs或限制传递给任何程序的命令行的长度-exec command {} +。来自man find

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

调用将少得多,但不能保证是一个。您应该做的是从stdin读取脚本中NUL分隔的文件名,这可能基于命令行参数-o -。我会做类似的事情:

$ find . -name something.txt -print0 | myscript -0 -o -

并相应地实现option参数myscript


是的,操作系统对可以传递的参数数量/大小施加了限制。在现代Linux系统上,这是(硕大的)(linux.die.net/man/2/execve)(堆栈大小的1 / 4,0x7FFFFFFF参数)。AFAIK bash本身没有施加任何限制。我的名单要小得多,而我的问题是由于误解或误解了xargs工作原理而引起的。您的解决方案确实是最强大的,但是在这种情况下,它是过分的。
Alexis 2014年

0

是否没有办法保护反引号(或$(...))扩展中的空间?

不,没有。这是为什么?

Bash无法知道应该保护什么,不应该保护什么。

Unix文件/管道中没有数组。这只是一个字节流。``或中的命令$()输出一个流,bash吞并并将其视为单个字符串。到那时,您只有两种选择:将其放在引号中,将其保留为一个字符串,或者将其裸露,以便bash根据其配置的行为将其拆分。

因此,如果要使用数组,您需要做的就是定义一个具有数组的字节格式,这就是工具所喜欢xargsfind要做的:如果使用-0参数运行它们,它们将按照二进制数组格式工作,该格式以空字节,将语义添加到不透明的字节流中。

不幸的是,bash无法配置为在空字节上拆分字符串。感谢/unix//a/110108/17980向我们展示了zsh

xargs

您希望您的命令运行一次,并说可以xargs -0 -n 10000解决您的问题。不会,它可以确保如果您有10000个以上的参数,则您的命令将运行多次。

如果要使其严格运行一次或失败,则必须提供-x参数和-n比该-s参数大的参数(确实:足够大,以至于一堆零长度的参数加上命令的名称不适合使用)的-s大小)。(man xargs,请参见下面的摘录)

我当前使用的系统的堆栈限制为8M,所以这是我的限制:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

重击

如果您不希望包含外部命令,那么/unix//a/110108/17980中所示的while循环读取数组将是bash在以下位置拆分内容的唯一方法空字节。

( . ... "$@" )为避免堆栈大小限制而获取脚本的想法很酷(我尝试过,它可以工作!),但对于正常情况而言可能并不重要。

如果您想从stdin中读取其他内容,则对过程管道使用特殊的fd非常重要,但否则就不需要它。

因此,最简单的“本机”方式可满足日常家庭需求:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

如果您喜欢干净的进程树并且看上去很漂亮,则可以使用此方法执行操作exec mynonscript "${files[@]}",该操作从内存中删除了bash进程,将其替换为被调用的命令。xargs即使被调用的命令仅运行一次,它也始终保留在内存中。


反对本机bash方法的是这样的:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash并未针对数组处理进行优化。


男人xargs

-n最大参数

每个命令行最多使用max-args参数。如果超过了大小(请参见-s选项),则使用少于max-args参数的参数,除非给出了-x选项,在这种情况下xargs将退出。

-s最大字符

每个命令行最多使用最大字符数字符,包括命令和初始参数以及参数字符串末尾的终止null。允许的最大值取决于系统,并且被计算为exec的参数长度限制,环境的大小和2048个字节的净空。如果此值大于128KiB,则将128Kib用作默认值;否则,将使用128Kib作为默认值。否则,默认值为最大值。1KiB是1024字节。

-X

如果超出大小(请参阅-s选项),请退出。


感谢所有麻烦,但您的基本前提忽略了bash 通常使用复杂的报价处理系统这一事实。但不是在反引号扩展中。比较以下内容(两者都会产生错误,但会显示出差异):ls "what is this"vs ls `echo '"what is this"'` 。有人忽略了对反引号的结果执行引号处理。
Alexis

我很高兴反引号不进行报价处理。他们甚至进行分词的事实在现代计算历史中引起了足够的混乱外观,头抓痕和安全漏洞。
clacke

问题是“是否有某种方法可以保护反引号(或$(...))扩展中的空间?”,因此似乎可以忽略在这种情况下不执行的处理。
clacke

以零结尾的元素数组格式是表示数组的最简单,因此也是最安全的方法。遗憾的是,bash它本来就不像它本来那样支持它zsh
clacke

实际上,就在本周,我使用了printf "%s\0"xargs -0绕过了这样一种报价情况:中间工具将通过外壳程序解析的字符串传递参数。报价总是会再次咬你。
clacke
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.