捕获find的输出。-print0进入bash数组


76

使用find . -print0似乎是获得在bash的文件列表的唯一可靠的方式因含有空格,换行,引号等文件名的可能性

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

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

本示例将打印找到的文件数,避免文件名中的换行符损坏计数的危险,如发生以下情况:

find . | wc -l

由于大多数命令行程序不支持以空分隔的输入,因此我认为最好的办法是find . -print0像在上面的perl片段中所做的那样,在bash数组中捕获输出,然后继续执行任务,无论如何是。

我怎样才能做到这一点?

这不起作用:

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

一个更普遍的问题可能是:如何使用bash中的文件列表来做有用的事情?


做有用的事情是什么意思?
2009年

4
哦,您知道,通常的事情对以下数组很有用:找出数组的大小;遍历它们的内容;向后打印出来;对它们进行排序。那种事 在UNIX中,有很多实用程序可以处理数据:wc,bash的for-loop,tac和sort;但是这些在处理可能包含空格或换行符的列表时似乎都没有用。即文件名。用空值输入字段分隔符来环绕数据似乎是解决方案,但是很少有实用程序可以处理此问题。
伊德里斯

1
这是一篇有关如何在shell中正确处理文件名的文章,其中有很多细节:http
David A. Wheeler,2010年

Answers:


103

从无耻地窃取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中执行它们,并设置任何变量(例如array a)退出时会丢失。 cmd1 < <(cmd2)仅在子外壳中运行cmd2,因此该阵列已超出其构造。警告:这种重定向形式仅在bash中可用,甚至在sh仿真模式下bash也不可用;您必须以开头脚本#!/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而不是stdin传递文件列表。


啊,快到了……这是最好的答案。但是,我只是在包含名称中包含换行符的文件的目录上进行了尝试,并且在使用echo $ {a [1]}检查该元素时,换行符似乎已经变成了空格(0x20)。知道为什么会这样吗?
伊德里斯

您正在运行什么版本的bash?我在使用较旧的版本时遇到了麻烦(不幸的是,我记不清是哪个版本)不处理换行符和\177字符串中的deletes()。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上进行了尝试;都将换行符转换成太空。
伊德里斯

11
-d ''等价于-d $'\0'
2011年

14
将元素添加到数组末尾的更简单方法是:arr+=("$file")
dogbane 2012年

7

也许您正在寻找xargs:

find . -print0 | xargs -r0 do_something_useful

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


2
这不是我所追求的,因为没有机会对列表执行类似数组的操作,例如排序:必须在find命令中使用每个元素。如果您可以详细说明此示例,并且“ do_something_useful”部分是bash数组推操作,那么这可能就是我想要的。
伊德里斯

5

主要问题在于,定界符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或类似的东西也可以完成这项工作。


1
必须确保查找到的文件系统部分从调用查找到脚本完成之间没有改变。如果不是这种情况,则会导致争用条件,该条件可被利用来调用错误文件上的命令。例如,要删除的目录(例如/ tmp / junk)可以由非特权用户替换为指向/ home的符号链接。如果find命令以root身份运行,并且是find -type d -exec rm -rf'{}'\ ;,则将删除所有用户的主文件夹。
黛咪(Demi)2013年

2
read -r -d ''会将所有内容读入下一个NUL中"$REPLY"。无需关心IFS
查尔斯·达菲

5

从Bash 4.4开始,内置函数mapfile具有-d开关(用于指定分隔符,类似于-dread语句的开关),并且分隔符可以为空字节。因此,标题中的问题很好的答案

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

是:

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

4

计数文件的另一种方法:

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

2

您可以使用以下方法安全地进行计数:

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

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


使用-printf选项而不是使用-exec每个文件的速度更快:find . -printf "\n" | wc -l
Oliver I,

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章


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

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 2014年

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

0

这类似于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[@]}

0

古老的问题,但是没有人提出这种简单的方法,所以我想我会的。当然,如果您的文件名具有ETX,这不能解决您的问题,但是我怀疑它可用于任何实际情况。尝试使用null似乎违反了默认的IFS处理规则。通过查找选项和错误处理来适应您的口味。

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

1
ETX是什么意思?也许是文件名EXT ension或文本结尾...
oHo,2016年

0

戈登·戴维森(Gordon Davisson)的答案非常有用。但是,对于zsh用户而言,存在一个有用的快捷方式:

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

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

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

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

有个窍门:^@是NUL字符。为此,您必须先输入Ctrl + V,再输入Ctrl + @。

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

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

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

B=( /tmp/** )

-1

Bash从未善于处理文件名(或实际上是任何文本),因为它使用空格作为列表定界符。

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

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.