如何循环查找由find返回的文件名?
x=$(find . -name "*.txt") echo $x
如果我在Bash shell中运行上面这段代码,我得到的是一个包含多个空格分隔的文件名的string,而不是一个列表。
当然,我可以进一步将它们分开,以获得一个清单,但我相信有一个更好的方法来做到这一点。
那么循环find
命令的结果的最好方法是什么?
TL; DR:如果您只是在这里寻求最正确的答案,那么您可能需要我的个人偏好, find . -name '*.txt' -exec process {} \;
答案find . -name '*.txt' -exec process {} \;
find . -name '*.txt' -exec process {} \;
(见这篇文章的底部)。 如果你有时间,阅读其余的部分,看看几种不同的方式和大多数的问题。
完整的答案:
最好的方法取决于你想要做什么,但这里有几个选项。 只要子树中没有文件或文件夹名称中有空格,就可以遍历文件:
for i in $x; do # Not recommended, will break on whitespace process "$i" done
稍微好一些,删去临时variablesx
:
for i in $(find -name \*.txt); do # Not recommended, will break on whitespace process "$i" done
如果可以的话更好。 白色空间安全,用于当前目录中的文件:
for i in *.txt; do # Whitespace-safe but not recursive. process "$i" done
通过启用globstar
选项,您可以将此目录中的所有匹配文件以及所有子目录:
# Make sure globstar is enabled shopt -s globstar for i in **/*.txt; do # Whitespace-safe and recursive process "$i" done
在某些情况下,例如,如果文件名已经在文件中,则可能需要使用read
:
# IFS= makes sure it doesn't trim leading and trailing whitespace # -r prevents interpretation of \ escapes. while IFS= read -r line; do # Whitespace-safe EXCEPT newlines process "$line" done < filename
通过适当地设置分隔符可以安全地结合使用read
:
find . -name '*.txt' -print0 | while IFS= read -r -d $'\0' line; do process $line done
对于更复杂的search,您可能希望使用find
,使用-exec
选项或-print0 | xargs -0
-print0 | xargs -0
:
# execute `process` once for each file find . -name \*.txt -exec process {} \; # execute `process` once with all the files as arguments*: find . -name \*.txt -exec process {} + # using xargs* find . -name \*.txt -print0 | xargs -0 process # using xargs with arguments after each filename (implies one run per filename) find . -name \*.txt -print0 | xargs -0 -I{} process {} argument
find
也可以cd到每个文件的目录之前运行一个命令通过使用-execdir
而不是-exec
,可以使用-ok
而不是-exec
(或-okdir
而不是-execdir
)。
*:从技术上来说, find
和xargs
(默认情况下)都会在命令行中使用尽可能多的参数来运行命令,而这个命令可以通过所有的文件。 在实践中,除非你有大量的文件,否则不会有问题,如果你超过了这个长度,但是需要它们在同一个命令行上, 那么SOLfind了一个不同的方法。
find . -name "*.txt"|while read fname; do echo "$fname" done
注意:这个方法和 bmargulies显示的(第二个)方法可以安全地在文件/文件夹名称中使用空格。
为了在文件/文件夹名称中包含换行符,也需要使用-exec
谓词,如下所示:
find . -name '*.txt' -exec echo "{}" \;
{}
是find的项目和\;
的占位符\;
用于终止-exec
谓词。
为了完整起见,我还需要添加另外一个变体 – 你必须非常喜欢* nix的多function性:
find . -name '*.txt' -print0|xargs -0 -n 1 echo
这将把打印的项目与文件或文件夹名称中的任何文件系统中不允许的\0
字符分开,据我所知,因此应覆盖所有的基础。 xargs
一个接一个地捡起来…
你做什么, 不要使用for
循环 :
# Don't do this for file in $(find . -name "*.txt") do …code using "$file" done
三个原因:
- for循环要启动,
find
必须运行完成。 - 如果一个文件名中有空格(包括空格,制表符或换行符),它将被视为两个单独的名字。
- 虽然现在不太可能,但是你可以溢出你的命令行缓冲区。 想象一下,如果你的命令行缓冲区容纳32KB,你的
for
循环返回40KB的文本。 最后的8KB将从你的for
循环中删除,你永远不会知道它。
总是使用一段while read
结构:
find . -name "*.txt" -print0 | while read -d $'\0' file do …code using "$file" done
循环将在find
命令执行时执行。 另外,即使文件名以空白字符返回,该命令也可以工作。 而且,你不会溢出你的命令行缓冲区。
-print0
将使用NULL作为文件分隔符而不是换行符, -d $'\0'
将在读取时使用NULL作为分隔符。
# Doesn't handle whitespace for x in `find . -name "*.txt" -print`; do process_one $x done or # Handles whitespace and newlines find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
文件名可以包含空格和控制字符。 空格是用于bash中shell扩展的(默认)分隔符,并且由于该问题的x=$(find . -name "*.txt")
而不被推荐。 如果find得到一个带有空格的文件名,例如"the file.txt"
你将得到2个分隔的string进行处理,如果你在一个循环中处理x
。 您可以通过更改分隔符(bash IFS
variables)来改善这一点,例如\r\n
,但文件名可以包含控制字符 – 所以这不是一个(完全)安全的方法。
从我的angular度来看,有2个build议(和安全)的模式处理文件:
1.使用循环和文件名扩展:
for file in ./*.txt; do [[ ! -e $file ]] && continue # continue, if file does not exist # single filename is in $file echo "$file" # your code here done
2.使用find-read-while和进程replace
while IFS= read -r -d '' file; do # single filename is in $file echo "$file" # your code here done < <(find . -name "*.txt" -print0)
备注
在模式1:
- 如果没有find匹配的文件,bash会返回search模式(“* .txt”),所以需要多余的行“continue,if file does not exist”。 请参阅Bash手册,文件名扩展
- shell选项
nullglob
可以用来避免这个额外的行。 - “如果设置了
failglob
shell选项,并且找不到匹配项,则会打印一条错误消息,并且不执行该命令。 (从上面的Bash手册) - shell选项
globstar
:“如果设置,文件扩展上下文中使用的模式**将匹配所有文件和零个或多个目录和子目录,如果模式后跟”/“,则只有目录和子目录匹配。 见Bash手册,Shopt Builtin - 文件名扩展的其他选项:
extglob
,nocaseglob
,dotglob
和shellvariablesGLOBIGNORE
模式2:
-
文件名可以包含空格,制表符,空格,换行符,以安全的方式处理文件名,使用
-print0
find
:文件名用所有控制字符打印并以NUL结尾。 请参阅Gnu Findutils联机帮助页,不安全的文件名处理 , 安全的文件名处理 , 文件名中的 不常用字符 。 有关此主题的详细讨论,请参阅下面的David A. Wheeler。 -
有一些可能的模式在while循环中处理查找结果。 其他人(kevin,David W.)展示了如何使用pipe道来做到这一点:
files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
当你尝试这段代码时,你会看到,它不起作用:
files_found
总是“真”,代码将总是回显“找不到文件”。 原因是:pipe道的每个命令都在一个单独的子shell中执行,所以在循环(独立的子shell)中更改的variables不会更改主shell脚本中的variables。 这就是为什么我build议使用stream程替代作为“更好”,更有用,更一般的模式。
请参阅我在stream水线中的循环中设置variables。 为什么他们消失… (来自格雷格的Bash常见问题)关于这个话题的详细讨论。
其他参考资料和来源:
-
Gnu Bash手册,模式匹配
-
Shell中的文件名和path名:如何正确执行David A. Wheeler
-
为什么你不读“格雷格的维基”的行
-
为什么你不应该分析格雷格的维基的ls(1)的输出
-
Gnu Bash手册,stream程替代
如果您希望稍后使用输出,则可以将find
输出存储在数组中:
array=($(find . -name "*.txt"))
现在要以新行打印每个元素,可以使用for
循环遍历数组的所有元素,也可以使用printf语句。
for i in ${array[@]};do echo $i; done
要么
printf '%s\n' "${array[@]}"
你也可以使用:
for file in "`find . -name "*.txt"`"; do echo "$file"; done
这将以换行方式打印每个文件名
要仅以列表forms打印find
输出,可以使用以下任一项:
find . -name "*.txt" -print 2>/dev/null
要么
find . -name "*.txt" -print | grep -v 'Permission denied'
这将删除错误消息,只给新文件名作为输出。
如果您希望使用文件名进行操作,则将其存储在数组中是不错的,否则不需要占用该空间,并且可以直接打印来自find
的输出。
任何支持它的$SHELL
(sh / bash / zsh / …):
find . -name "*.txt" -exec $SHELL -c ' echo "$0" ' {} \;
完成。
假设你没有embedded换行符的文件名,你可以得到像这样的列表:
list=($(find . -name '*.txt')) printf '%s\n' "${list[@]}"
正如其他人所指出的,这是否有用取决于上下文。
如果您可以假定文件名不包含换行符,则可以使用readarray
命令将find
的输出读取到Bash数组中:
readarray -tx < <(find . -name '*.txt')
注意:
-
-t
导致readarray
换行符。 - 如果
readarray
在一个pipe道中,那么readarray
,因此进程replace。
find <path> -xdev -type f -name *.txt -exec ls -l {} \;
这将列出文件并提供有关属性的详细信息。
基于@phk的其他答案和评论,使用fd#3:
(它仍然允许在循环中使用stdin)
while IFS= read -rf <&3; do echo "$f" done 3< <(find . -iname "*filename*")
你可以把find
返回的文件名放到这样的数组中:
array=() while IFS= read -r -d $'\0'; do array+=("$REPLY") done < <(find . -name '*.txt' -print0)
现在,您可以循环访问数组来访问单个项目,并根据需要执行任何操作。
注意:这是白色空间安全。
我喜欢使用第一个分配给variables的查找,IFS切换到新行,如下所示:
FilesFound=$(find . -name "*.txt") IFSbkp="$IFS" IFS=$'\n' counter=1; for file in $FilesFound; do echo "${counter}: ${file}" let counter++; done IFS="$IFSbkp"
以防万一你想在同一组数据上重复更多的操作,并且发现在你的服务器上很慢(I / 0的高利用率)
如果你使用grep而不是find,那么怎么样?
ls | grep .txt$ > out.txt
现在你可以阅读这个文件,文件名是一个列表的forms。