捕获 find 的输出。 -print0 到 bash 数组中

Posted

技术标签:

【中文标题】捕获 find 的输出。 -print0 到 bash 数组中【英文标题】:Capturing output of find . -print0 into a bash array 【发布时间】:2010-11-10 03:51:07 【问题描述】:

使用find . -print0 似乎是在 bash 中获取文件列表的唯一安全方法,因为文件名可能包含空格、换行符、引号等。

但是,我很难真正让 find 的输出在 bash 或其他命令行实用程序中有用。我设法利用输出的唯一方法是将其通过管道传输到 perl,并将 perl 的 IFS 更改为 null:

find . -print0 | perl -e '$/="\0"; @files=<>; print $#files;'

此示例打印找到的文件数,避免文件名中的换行符破坏计数的危险,如下所示:

find . | wc -l

由于大多数命令行程序不支持以 null 分隔的输入,我认为最好的办法是在 bash 数组中捕获 find . -print0 的输出,就像我在上面的 perl sn-p 中所做的那样,然后继续任务,不管它是什么。

我该怎么做?

这不起作用:

find . -print0 | ( IFS=$'\0' ; array=( $( cat ) ) ; echo $#array[@] )

一个更普遍的问题可能是:我怎样才能用 bash 中的文件列表做有用的事情?

【问题讨论】:

做有用的事情是什么意思? 哦,你知道,数组通常有用的地方是:找出它们的大小;迭代它们的内容;向后打印它们;对它们进行排序。那种事。 unix 中有很多实用程序可以处理数据:wc、bash 的 for 循环、tac 和 sort;但是在处理可能包含空格或换行符的列表时,这些似乎都没有用。 IE。文件名。使用空值输入字段分隔符传递数据似乎是解决方案,但很少有实用程序可以处理这个问题。 这是一篇关于如何在 shell 中正确处理文件名的文章,有很多细节:http://www.dwheeler.com/essays/filenames-in-shell.html 【参考方案1】:

无耻从Greg's BashFAQ盗取:

unset a i
while IFS= read -r -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done < <(find /tmp -type f -print0)

请注意,这里使用的重定向构造 (cmd1 &lt; &lt;(cmd2)) 与更常用的管道 (cmd2 | cmd1) 相似,但并不完全相同——如果命令是 shell 内置命令(例如 while),则管道版本在子shell中执行它们,它们设置的任何变量(例如数组a)在它们退出时都会丢失。 cmd1 &lt; &lt;(cmd2) 仅在子外壳中运行 cmd2,因此该数组在其构造之后仍然存在。警告:这种形式的重定向只在 bash 中可用,甚至在 sh-emulation 模式下也不可用;您必须以 #!/bin/bash 开始您的脚本。

另外,因为文件处理步骤(在这种情况下,只是a[i++]="$file",但您可能想直接在循环中做一些更有趣的事情)的输入重定向,它不能使用任何可能从标准输入读取的命令。为了避免这个限制,我倾向于使用:

unset a i
while IFS= read -r -u3 -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done 3< <(find /tmp -type f -print0)

...通过单元 3 传递文件列表,而不是标准输入。

【讨论】:

啊,快到了……这是迄今为止最好的答案。但是,我刚刚在包含名称中带有换行符的文件的目录上尝试过它,并且在使用 echo $a[1] 检查该元素时,换行符似乎已变成空格(0x20)。知道为什么会这样吗? 您运行的是哪个版本的 bash?我在处理字符串中的换行符和删除 (\177) 的旧版本时遇到了麻烦(不幸的是,我不记得具体是哪个)。 IIRC,即使 x="$y" 也不总是适用于这些字符。我刚刚用 bash 2.05b.0 和 3.2.17 (我手边最旧和最新的)进行了测试;两者都正确处理换行符,但 v2.05b.0 吃了删除字符。 我已经在 osx 上的 3.2.17、linux 上的 3.2.39 和 netBSD 上的 3.2.48 上尝试过;都把换行符变成空格。 -d '' 等价于-d $'\0' 将元素添加到数组末尾的更简单方法是:arr+=("$file")【参考方案2】:

从 Bash 4.4 开始,内置的mapfile-d 开关(用于指定分隔符,类似于read 语句的-d 开关),分隔符可以是空字节。因此,很好地回答了标题中的问题

find . -print0 的输出捕获到 bash 数组中

是:

mapfile -d '' ary < <(find . -print0)

【讨论】:

这看起来更优雅,而且还可以作为定位的魅力:mapfile -d '' list &lt; &lt;(locate -b -0 -r "$1$")【参考方案3】:

也许你正在寻找 xargs:

find . -print0 | xargs -r0 do_something_useful

选项 -L 1 也可能对您有用,这使得 xargs exec do_something_useful 只需 1 个文件参数。

【讨论】:

这不是我想要的,因为没有机会对列表进行类似数组的操作,例如排序:您必须在每个元素出现时使用它查找命令。如果您可以详细说明此示例,其中“do_something_useful”部分是 bash 数组推送操作,那么这可能就是我所追求的。【参考方案4】:

主要问题是,分隔符 NUL (\0) 在这里没有用,因为不可能为 IFS 分配 NUL 值。因此,作为优秀的程序员,我们要注意程序的输入是它能够处理的。

首先我们创建一个小程序,它会为我们完成这部分工作:

#!/bin/bash
printf "%s" "$@" | base64

...称之为base64str(别忘了chmod +x)

其次,我们现在可以使用简单直接的 for 循环:

for i in `find -type f -exec base64str '' \;`
do 
  file="`echo -n "$i" | base64 -d`"
  # do something with file
done

所以诀窍是,base64 字符串没有标志,这会给 bash 带来麻烦——当然,xxd 或类似的东西也可以完成这项工作。

【讨论】:

必须确保 find 正在处理的文件系统部分从调用 find 到脚本完成时不会发生变化。如果不是这种情况,则会产生竞争条件,可以利用它来调用错误文件的命令。例如,要删除的目录(例如 /tmp/junk)可以由非授权用户替换为 /home 的符号链接。如果 find 命令以 root 身份运行,并且它是 find -type d -exec rm -rf '' \;,这将删除所有用户的主文件夹。 read -r -d '' 会将直到下一个 NUL 的所有内容读入"$REPLY"。无需关心IFS【参考方案5】:

另一种计算文件的方法:

find /DIR -type f -print0 | tr -dc '\0' | wc -c 

【讨论】:

【参考方案6】:

你可以放心地用这个来计数:

find . -exec echo ';' | wc -l

(它为找到的每个文件/目录打印一个换行符,然后计算打印出的换行符...)

【讨论】:

对每个文件使用-printf 选项而不是-exec 要快得多:find . -printf "\n" | wc -l【参考方案7】:

我认为存在更优雅的解决方案,但我将把它扔进去。这也适用于带有空格和/或换行符的文件名:

i=0;
for f in *; do
  array[$i]="$f"
  ((i++))
done

然后你可以例如一个一个地列出文件(在这种情况下以相反的顺序):

for ((i = $i - 1; i >= 0; i--)); do
  ls -al "$array[$i]"
done

This page 给出了一个很好的例子,更多信息请参见Advanced Bash-Scripting Guide 中的Chapter 26。

【讨论】:

这(以及下面的其他类似示例)几乎是我所追求的 - 但有一个大问题:它仅适用于当前目录的 glob。我希望能够操作完全任意的文件列表;例如“find”的输出,它递归地列出目录,或任何其他列表。如果我的列表是:( /tmp/foo.jpg | /home/alice/bar.jpg | /home/bob/my holiday/baz.jpg | /tmp/new\nline/grault.jpg )或任何其他完全任意的文件列表(当然,其中可能包含空格和换行符)?【参考方案8】:

尽可能避免使用 xargs:

man ruby | less -p 777 
IFS=$'\777' 
#array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '' \; 2>/dev/null) ) 
array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '' + 2>/dev/null) ) 
echo $#array[@] 
printf "%s\n" "$array[@]" | nl 
echo "$array[0]" 
IFS=$' \t\n' 

【讨论】:

为什么将IFS设置为\777【参考方案9】:

我是新手,但我相信这是一个答案;希望它可以帮助某人:

STYLE="$HOME/.fluxbox/styles/"

declare -a array1

LISTING=`find $HOME/.fluxbox/styles/ -print0 -maxdepth 1 -type f`


echo $LISTING
array1=( `echo $LISTING`)
TAR_SOURCE=`echo $array1[@]`

#tar czvf ~/FluxieStyles.tgz $TAR_SOURCE

【讨论】:

【参考方案10】:

Gordon Davisson 的回答非常适合 bash。然而,对于 zsh 用户来说,存在一个有用的快捷方式:

首先,将字符串放入变量中:

A="$(find /tmp -type f -print0)"

接下来,拆分这个变量并将其存储在一个数组中:

B=( $(s/^@/)A )

有一个技巧:^@ 是 NUL 字符。为此,您必须键入 Ctrl+V,然后键入 Ctrl+@。

您可以检查 $B 的每个条目是否包含正确的值:

for i in "$B[@]"; echo \"$i\"

细心的读者可能会注意到,在大多数情况下,使用** 语法可以避免调用find 命令。例如:

B=( /tmp/** )

【讨论】:

【参考方案11】:

这类似于 Stephan202 的版本,但文件(和目录)被一次性放入一个数组中。这里的for 循环只是为了“做有用的事情”:

files=(*)                        # put files in current directory into an array
i=0
for file in "$files[@]"
do
    echo "File $i: $file"    # do something useful 
    let i++
done

要计数:

echo $#files[@]

【讨论】:

【参考方案12】:

老问题,但没有人建议这种简单的方法,所以我想我会的。如果你的文件名有 ETX,这并不能解决你的问题,但我怀疑它适用于任何现实世界的场景。尝试使用 null 似乎违反了默认的 IFS 处理规则。使用查找选项和错误处理根据您的口味调整。

savedFS="$IFS"
IFS=$'\x3'
filenames=(`find wherever -printf %p$'\x3'`)
IFS="$savedFS"

【讨论】:

ETX 是什么意思?也许是文件名 EXTension 或者 End of Text...【参考方案13】:

Bash 从来不擅长处理文件名(或任何文本),因为它使用空格作为列表分隔符。

我建议将 python 与 sh 库一起使用。

【讨论】:

以上是关于捕获 find 的输出。 -print0 到 bash 数组中的主要内容,如果未能解决你的问题,请参考以下文章

将系统命令的输出捕获到文本文件的最佳方法?

PyDev unittesting:如何在“捕获的输出”中捕获记录到 logging.Logger 的文本

shell编程题

捕获 JSP 输出,保存到文件

Powershell:捕获组合输出,仅错误输出,将组合输出发送到控制台

致命错误:未捕获的错误:在解析数据时调用字符串上的成员函数 find()