捕获 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 < <(cmd2)
) 与更常用的管道 (cmd2 | cmd1
) 相似,但并不完全相同——如果命令是 shell 内置命令(例如 while
),则管道版本在子shell中执行它们,它们设置的任何变量(例如数组a
)在它们退出时都会丢失。 cmd1 < <(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 < <(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 的文本