如何循环查找返回的文件名?

x=$(find . -name "*.txt")
echo $x

如果我在 Bashshell 中运行上面的代码,我得到的是一个包含几个由空格分隔的文件名的字符串,而不是一个列表。

当然,我可以进一步用空格将它们分开,以得到一个列表,但我确信有更好的方法来做到这一点。

那么,循环执行 find命令的结果的最佳方式是什么呢?

386344 次浏览
find . -name "*.txt"|while read fname; do
echo "$fname"
done

注意:此方法而且 bmargulies所示的(第二个)方法在文件/文件夹名称中使用空白时是安全的。

为了在文件/文件夹名称中包含换行符,你必须像这样使用find-exec谓词:

find . -name '*.txt' -exec echo "{}" \;

{}是找到的项的占位符,\;用于终止-exec谓词。

为了完整起见,让我添加另一个变体-你必须喜欢*nix的多功能性:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

这将用\0字符分隔打印项,据我所知,在任何文件系统的文件或文件夹名称中都不允许使用\0字符,因此应该涵盖所有基础。xargs接他们一个接一个然后…

# 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

TL;DR:如果你只是想知道最正确的答案,你可能想知道我的个人偏好(见本文底部):

# execute `process` once for each file
find . -name '*.txt' -exec process {} \;

如果有时间,请通读其余部分,了解几种不同的方法以及其中大多数方法的问题。


完整的答案是:

最好的方法取决于你想做什么,但这里有一些选择。只要子树中没有文件名中有空格的文件或文件夹,你就可以遍历这些文件:

for i in $x; do # Not recommended, will break on whitespace
process "$i"
done

稍微好一点,去掉临时变量x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
process "$i"
done

最好是在你可以的时候glob。空白安全,对于当前目录中的文件:

for i in *.txt; do # Whitespace-safe but not recursive.
process "$i"
done

通过启用globstar选项,你可以glob这个目录和所有子目录中所有匹配的文件:

# 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结合使用:

find . -name '*.txt' -print0 |
while IFS= read -r -d '' line; do
process "$line"
done

对于更复杂的搜索,你可能会想要使用find,或者带它的-exec选项,或者带-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

通过使用-execdir而不是-execfind也可以在运行命令之前cd到每个文件的目录中,并且可以使用-ok而不是-exec(或-okdir而不是-execdir)来进行交互(在为每个文件运行命令之前提示)。

*:从技术上讲,findxargs(默认情况下)都将运行命令,命令行中包含尽可能多的参数,以及遍历所有文件所需的次数。在实践中,除非你有非常多的文件,否则这并不重要,如果你超过了长度,但需要它们都在同一个命令行上,你是索尔找到不同的方法。

不管你做什么,不要使用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作为分隔符。

如果使用grep而不是find呢?

ls | grep .txt$ > out.txt

现在您可以读取这个文件,文件名以列表的形式出现。

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

这将列出文件并给出有关属性的详细信息。

你可以将你的find输出存储在数组中,如果你希望以后使用输出:

array=($(find . -name "*.txt"))

现在要打印new line中的每个元素,可以使用for循环迭代数组的所有元素,也可以使用printf语句。

for i in ${array[@]};do echo $i; done

printf '%s\n' "${array[@]}"

你还可以使用:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

这将以换行符打印每个文件名

要仅以列表形式打印find输出,可以使用以下方法之一:

find . -name "*.txt" -print 2>/dev/null

find . -name "*.txt" -print | grep -v 'Permission denied'

这将删除错误消息,并仅在新行中输出文件名。

如果你想对文件名做一些事情,将它存储在数组中是很好的,否则就不需要占用这个空间,你可以直接打印find的输出。

如果你可以假设文件名不包含换行符,你可以使用以下命令将find的输出读入一个Bash数组:

readarray -t x < <(find . -name '*.txt')

注意:

  • -t导致readarray去掉换行符。
  • 如果readarray在管道中,它将不起作用,因此是进程替换。
  • readarray自Bash 4起可用。

Bash 4.4及更高版本还支持-d参数来指定分隔符。使用空字符而不是换行符来分隔文件名也适用于文件名包含换行符的罕见情况:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarray也可以被调用为具有相同选项的mapfile

参考:https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

你可以将find返回的文件名放入这样的数组中:

array=()
while IFS=  read -r -d ''; do
array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

现在,您可以循环遍历数组以访问单个项,并对它们做任何您想做的事情。

这是空白安全的。

根据@phk的其他答案和评论,使用fd #3:
(仍然允许在循环中使用stdin)

while IFS= read -r f <&3; do
echo "$f"


done 3< <(find . -iname "*filename*")

文件名可以包括空格甚至控制字符。空格是bash中shell扩展的(默认)分隔符,因此根本不建议使用问题中的x=$(find . -name "*.txt")。如果find得到一个带有空格的文件名,例如"the file.txt",如果在循环中处理x,则将得到两个分开的字符串进行处理。你可以通过改变分隔符(bash IFS Variable)来改善这一点,例如\r\n,但文件名可以包括控制字符-所以这不是一个(完全)安全的方法。

从我的角度来看,有两种推荐的(安全的)文件处理模式:

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 &进程替换

while IFS= read -r -d '' file; do
# single filename is in $file
echo "$file"
# your code here
done < <(find . -name "*.txt" -print0)

讲话

模式1:

  1. 如果没有找到匹配的文件,Bash返回搜索模式("*.txt") -因此,如果文件不存在,那么额外的行"继续;是必要的。看到Bash手册,文件名扩展
  2. shell选项nullglob可以用来避免这额外的行。
  3. 如果设置了failglob shell选项,并且没有找到匹配项,则打印错误消息并且不执行命令。(来自上面的Bash手册)
  4. 如果设置了,文件名扩展上下文中使用的模式' ** '将匹配所有文件和零个或多个目录和子目录。如果模式后面跟着' / ',则只有目录和子目录匹配。"看到Bash手册,内置商店
  5. 文件名扩展的其他选项:extglobnocaseglobdotglob &shell变量GLOBIGNORE

模式二:

  1. 文件名可以包含空格,制表符,空格,换行符,…为了以一种安全的方式处理文件名,使用find-print0: filename打印所有控制字符&以NUL终止。参见Gnu Findutils Manpage,不安全的文件名处理安全文件名处理文件名中不寻常的字符。关于这个主题的详细讨论,请参阅下面的David A. Wheeler。

  2. 在while循环中有一些可能的模式来处理查找结果。其他人(kevin, David W.)已经展示了如何使用管道来做到这一点:

    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总是“true”;,代码将始终返回“没有找到文件”。原因是:管道的每个命令都是在一个单独的subshell中执行的,所以循环中的变量(单独的subshell)不会改变主shell脚本中的变量。这就是为什么我推荐使用过程替代作为“更好”、更有用、更通用的模式。
    参见我在管道中的循环中设置变量。为什么它们消失了……(来自Greg的Bash常见问题解答)关于此主题的详细讨论。

其他参考资料来源:

(更新到包括@Socowi的卓越速度提升)

使用任何支持它的$SHELL (dash/zsh/bash…):

find . -name "*.txt" -exec $SHELL -c '
for i in "$@" ; do
echo "$i"
done
' {} +

完成了。


原来的答案(更短,但更慢):

find . -name "*.txt" -exec $SHELL -c '
echo "$0"
' {} \;

我喜欢使用find,这是第一次分配给变量和IFS切换到新行如下:

FilesFound=$(find . -name "*.txt")


IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
echo "${counter}: ${file}"
let counter++;
done
IFS="$IFSbkp"

正如@Konrad Rudolph所评论的那样,这将不适用于“新线”;文件名。我仍然认为它很方便,因为它涵盖了需要遍历命令输出的大多数情况。

function loop_through(){
length_="$(find . -name '*.txt' | wc -l)"
length_="${length_#"${length_%%[![:space:]]*}"}"
length_="${length_%"${length_##*[![:space:]]}"}"
for i in {1..$length_}
do
x=$(find . -name '*.txt' | sort | head -$i | tail -1)
echo $x
done


}
为了获取循环文件列表的长度,我使用了第一个命令& wc -l"< br > 该命令被设置为一个变量。< br > 然后,我需要从变量中删除尾随的空格,以便for循环可以读取它。< br > < / p >

正如凯文已经在顶部的答案中发布的那样,最好的解决方案是使用带有bash glob的for循环,但由于bash glob默认情况下不是递归的,这可以通过bash递归函数来修复:

#!/bin/bash
set -x
set -eu -o pipefail


all_files=();


function get_all_the_files()
{
directory="$1";
for item in "$directory"/* "$directory"/.[^.]*;
do
if [[ -d "$item" ]];
then
get_all_the_files "$item";
else
all_files+=("$item");
fi;
done;
}


get_all_the_files "/tmp";


for file_path in "${all_files[@]}"
do
printf 'My file is "%s"\n' "$file_path";
done;

相关问题:

  1. Bash循环通过目录,包括隐藏文件
  2. 在Bash中递归地列出给定目录中的文件
  3. ls命令:我如何才能得到一个递归的全路径列表,每个文件一行?< / >
  4. 在Linux CLI中递归列出相对于当前目录的文件
  5. 递归地列出所有目录和文件
  6. bash脚本,创建数组的所有文件在一个目录
  7. 如何创建包含文件夹中所有文件名称的数组?< / >
  8. 如何创建包含文件夹中所有文件名称的数组?< / >
  9. 如何在shell脚本中获得目录中的文件列表?< / >

另一种选择是不使用bash,而是调用Python来完成繁重的工作。我反复使用这个方法是因为bash解决方案作为我的另一个答案太慢了。

使用这个解决方案,我们从内联Python脚本构建一个bash文件数组:

#!/bin/bash
set -eu -o pipefail


dsep=":"  # directory_separator
base_directory=/tmp


all_files=()
all_files_string="$(python3 -c '#!/usr/bin/env python3
import os
import sys


dsep="'"$dsep"'"
base_directory="'"$base_directory"'"


def log(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)


def check_invalid_characther(file_path):
for thing in ("\\", "\n"):
if thing in file_path:
raise RuntimeError(f"It is not allowed {thing} on \"{file_path}\"!")
def absolute_path_to_relative(base_directory, file_path):
relative_path = os.path.commonprefix( [ base_directory, file_path ] )
relative_path = os.path.normpath( file_path.replace( relative_path, "" ) )


# if you use Windows Python, it accepts / instead of \\
# if you have \ on your files names, rename them or comment this
relative_path = relative_path.replace("\\", "/")
if relative_path.startswith( "/" ):
relative_path = relative_path[1:]
return relative_path


for directory, directories, files in os.walk(base_directory):
for file in files:
local_file_path = os.path.join(directory, file)
local_file_name = absolute_path_to_relative(base_directory, local_file_path)


log(f"local_file_name {local_file_name}.")
check_invalid_characther(local_file_name)
print(f"{base_directory}{dsep}{local_file_name}")
' | dos2unix)";
if [[ -n "$all_files_string" ]];
then
readarray -t temp <<< "$all_files_string";
all_files+=("${temp[@]}");
fi;


for item in "${all_files[@]}";
do
OLD_IFS="$IFS"; IFS="$dsep";
read -r base_directory local_file_name <<< "$item"; IFS="$OLD_IFS";


printf 'item "%s", base_directory "%s", local_file_name "%s".\n' \
"$item" \
"$base_directory" \
"$local_file_name";
done;

相关:

  1. os.walk不带隐藏文件夹 .walk
  2. 如何在列表中进行递归子文件夹搜索和返回文件?< / >
  3. 如何将一个字符串分割成一个数组在Bash?< / >

我认为使用这段代码(将命令输送到while done之后):

while read fname; do
echo "$fname"
done <<< "$(find . -name "*.txt")"

这个答案更好,因为while循环是根据在这里在子shell中执行的,如果你使用这个答案,并且如果你想修改循环内的变量,则在while循环后无法看到变量的变化。