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

82

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

然而,我很难在Bash或其他命令行工具中实际利用find的输出。我唯一成功利用输出的方法是将其管道到perl,并将perl的IFS更改为空值:

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

这个示例打印找到的文件数,避免了文件名中换行符对计数造成的破坏,而这种情况会发生在以下代码中:

find . | wc -l

由于大多数命令行程序不支持空字符分隔的输入,我认为最好的方法是像我在上面的Perl片段中所做的那样,在bash数组中捕获find . -print0的输出,然后继续处理任务。

我该如何做到这一点?

这种方法行不通:

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

一个更加普遍的问题可能是:如何在bash中使用文件列表来执行有用的操作?


你说的“做有用的事情”是什么意思? - Balázs Pozsár
5
哦,你知道,数组通常用于以下方面:查找它们的大小;遍历它们的内容;倒序打印它们;排序。那种事情。Unix中有大量实用程序可用于对数据进行这些操作:wc、bash的for循环、tac和sort等;但当处理可能包含空格或换行符的列表时,这些都似乎毫无用处。也就是说,文件名。使用空值输入字段分隔符传输数据似乎是解决方案,但很少有实用工具可以处理这种情况。 - Idris
1
这是一篇关于如何在shell中正确处理文件名的文章,包含很多具体细节:http://www.dwheeler.com/essays/filenames-in-shell.html - David A. Wheeler
13个回答

110

不要介意从Greg的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 < <(cmd2))类似于但不完全等同于更常见的管道结构(cmd2 | cmd1)。如果命令是shell内置命令(例如while),则管道版本在子shell中执行它们,它们设置的任何变量(例如数组a)在退出时都会丢失。 cmd1 < <(cmd2) 仅在子shell中运行cmd2,因此该数组存在于其构造后。警告:此形式的重定向仅在bash中可用,甚至不在sh模拟模式下的bash中;您必须以#!/bin/bash开始脚本。

另外,由于文件处理步骤(在本例中只有 a[i++]="$file",但您可能想要直接在循环中做一些更高级的事情)已经将其输入重定向,因此它不能使用任何可能从stdin读取的命令。为了避免这种限制,我倾向于使用:

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)

该方法通过第三个单元以文件列表的方式传递,而非通过标准输入(stdin)。


15
-d '' 相当于 -d $'\0' - l0b0
16
向数组末尾添加元素的更简单方式是:arr+=("$file") - dogbane
使用 #!/bin/bash(而不是在 sh 模拟模式下使用 bash)的警告对于重定向非常有帮助。 - kenj
@GordonDavisson,您不能依赖于静态文件描述符“3”,因为它可能已经被使用。相反,您可以按照这里的说明分配下一个可用的文件描述符:https://dev59.com/E2sy5IYBdhLWcg3w0RXT#17030546这是一个综合示例:https://blog.famzah.net/2016/10/20/bash-process-null-terminated-results-piped-from-external-commands/ - famzah
1
@CMCDragonkai:readarray是在bash 4版本中添加的,而当我写这个答案时,该版本刚刚发布。一些操作系统(咳咳 macOS 咳咳)仍然使用bash v3,因此不能安全地假设readarray可用。因此,我实际上还没有完成需要的工作,以找出readarray可能存在的问题,并了解如何避免它们。如果我有时间,我会更新答案。 - Gordon Davisson
显示剩余5条评论

15
自从Bash 4.4以来,内置命令mapfile有了-d开关(用于指定分隔符,类似于read语句的-d开关),而且分隔符可以是空字节。因此,对于标题中的问题,一个不错的答案是:

find . -print0命令的输出捕获到Bash数组中

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

3
看起来更加优雅,而且对于 locate 来说也非常有效:mapfile -d '' list < <(locate -b -0 -r "$1$") - user unknown
这个答案是正确和优雅的,虽然我犯了重新排序mapfile参数的错误: mapfile ary -d'' 不做相同的事情。 - Jonathan Mayer

6
主要问题是,分隔符 NUL (\0) 在这里是无用的,因为不可能将 IFS 赋值为 NUL 值。作为优秀的程序员,我们会注意确保程序的输入可被处理。
首先,我们创建一个小程序来处理这个部分:
#!/bin/bash
printf "%s" "$@" | base64

第一步,我们需要把它编码为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或类似的工具也可以完成任务。

1
必须确保在调用find命令时,find正在处理的文件系统部分在脚本完成之前不会发生更改。如果不是这种情况,则会导致竞争条件,从而可以利用错误的文件上调用命令。例如,一个要删除的目录(比如/tmp/junk)可能会被非特权用户替换为指向/home的符号链接。如果find命令以root身份运行,并且它是find -type d -exec rm -rf '{}' ;,那么这将删除所有用户的主文件夹。 - Demi
3
read -r -d '' 会将接下来的所有内容读取到 "$REPLY" 中,直到遇到下一个 NUL 字符。不需要关心 IFS - Charles Duffy
这取决于你使用的shell吧?在bash 5.2.15中,read -r -d ''会产生bash: warning: command substitution: ignored null byte in input的警告。 - undefined

6
也许你正在寻找xargs命令:
find . -print0 | xargs -r0 do_something_useful

选项-L 1也对你有用,它只使用一个文件参数使xargs exec do_something_useful。

4
这不是我想要的,因为在列表中没有机会像数组一样进行操作,例如排序:您必须在find命令返回每个元素时使用它。如果您可以详细说明此示例,其中“do_something_useful”部分是一个bash数组推操作,那么这可能就是我想要的。 - Idris

4

另一种计算文件的方法:

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

1

我认为存在更优雅的解决方案,但我会提供这个。这也适用于带有空格和/或换行符的文件名:

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

然后,您可以逐个列出文件(在这种情况下按相反顺序):

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

这个页面提供了一个很好的例子,更多内容请参见高级Bash脚本指南中的第26章


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

1

虽然这是一个老问题,但没有人提供这种简单的方法,所以我想我可以提供一下。如果你的文件名有ETX,那么这种方法无法解决你的问题,但我认为它适用于任何真实世界的场景。尝试使用null似乎违反了默认IFS处理规则。根据需要使用find选项和错误处理。

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

2
什么是 ETX?也许是文件名的 EXTension,或者可能意味着 End of Text(文本结束)。 - oHo
ETX 是 ASCII 字符 #3,表示为 '\x3'。"End of Text" - Chris Combs

1

我是新手,但我相信这是一个答案;希望能帮到某些人:

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

1

你可以放心使用以下代码进行计数:

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

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

2
使用-printf选项而不是为每个文件使用-exec选项要快得多: find . -printf“\n”| wc -l - Oliver I

1
避免使用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 - sschober

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接