如何在Bash中仅循环遍历目录?

我有一个文件夹,里面有一些目录和一些文件(其中一些是隐藏的,以点开头)。
for d in *; do
 echo $d
done

循环遍历所有文件和目录,但我只想循环遍历目录。我该如何做到这一点?
14个回答

您可以在末尾指定斜杠以仅匹配目录:
for d in */ ; do
    echo "$d"
done

如果你想排除符号链接,可以使用一个测试来在当前条目是链接时跳过循环。为了让“-L”能够识别它是一个符号链接,你需要从名称中去掉末尾的斜杠。
for d in */ ; do
    [ -L "${d%/}" ] && continue
    echo "$d"
done

2你可以使用 for d in */ .*/ : do ... 循环遍历隐藏文件。但是如何在循环中排除 ../ - rubo77
只使用物理系统(而不是符号链接),另一个选项是 'set -P'。 - AsymLabs
1@AsymLabs 这是不正确的,set -P 只会影响改变目录的命令。http://sprunge.us/TNac - Chris Down
如果你想让这个变成递归,你可以使用 **/ - Aaron
1有人能更详细地解释一下 */ ; 吗?我对代码的作用和功能还不太理解。 - timbram
3@timbram:*是通配符,代表“任何东西”。/表示“任何东西”必须是一个目录才能匹配通配符表达式。;是for循环的一部分,它将dofor分开。 - choroba
3在当前文件夹中添加shopt -s nullglob以防止没有目录时出现问题。 - rubo77
对于那些好奇的人,此答案打印目录的“相对”名称(例如 dir1),而不是完整的目录路径(例如 /home/user/dir1)。 - Raleigh L.

你可以用-d进行测试。
for f in *; do
    if [ -d "$f" ]; then
        # $f is a directory
    fi
done

这是文件测试运算符中的一个。

10请注意,翻译的文本中将包含对目录的符号链接。 - Stéphane Chazelas
34如果 [[ -d "$f" && ! -L "$f" ]],将排除符号链接。 - rubo77
1如果目录为空,将会中断。if [[ "$f" = "*" ]]; then continue; fi - Piskvor left the building
如果当前目录为空,则-d测试将为假,所以不用担心,也不必与模式进行比较。 - Kusalananda

小心,虽然choroba的解决方案很巧妙,但如果当前目录中没有可用的目录,可能会引发意外行为。在这种情况下,bash不会跳过for循环,而是会运行一次循环,其中d等于*/。
#!/usr/bin/env bash

for d in */; do
    # Will print */ if no directories are available
    echo "$d"
done

我建议使用以下方法来防止这种情况:
#!/usr/bin/env bash

for f in *; do
    if [ -d "$f" ]; then
        # Will not run if no directories are available
        echo "$f"
    fi
done

这段代码将循环遍历当前目录中的所有文件,检查f是否为一个目录,如果条件返回true,则输出f。如果f等于*/,则echo "$f"将不会执行。

12更容易的方法是使用shopt -s nullglob命令。 - choroba

如果你需要选择比仅限于目录更具体的文件,使用find命令,并将其传递给while read
shopt -s dotglob
find * -prune -type d | while IFS= read -r d; do 
    echo "$d"
done

使用shopt -u dotglob来排除隐藏目录(或在zsh中使用setopt dotglob/unsetopt dotglob)。

IFS=用于避免拆分包含$IFS之一的文件名,例如:'a b'

有关更多find选项,请参见下方的AsymLabs答案


编辑:
如果你需要在while循环内部创建一个退出值,你可以通过这个技巧避免额外的子shell。
while IFS= read -r d; do 
    if [ "$d" == "something" ]; then exit 1; fi
done < <(find * -prune -type d)

3我在这里找到了解决方案:http://stackoverflow.com/a/8489394/1069083 - rubo77
1不要循环遍历find的输出。即使将IFS设置为空字符串,它仍然会在包含换行符的文件名上出错。尽管这些文件名很少见,但如果能够轻松编写处理所有文件名的代码,那就没有理由编写不能处理的代码。 - Kusalananda
@Kusalananda “不要循环遍历find的输出”是误导性的。只要输出元素不以换行符(默认)而是以空字符结尾,这样做是可以的。这将需要将IFS更改为0字节,或者使用read -d来处理它。然后,即使文件名包含换行符,也可以正确处理。 - Jonathan Komar
@JonathanKomar 这个回答并没有误导,因为其中没有采取任何预防措施。将IFS更改为包含空字符意味着可以在变量中存储这些字符的shell。而bash shell并不支持这样做。read -d是特定于bash的(xargs -0可能更适合,因为它不依赖于shell,尽管它仍然不是标准的)。我的观点是,如果你能以一种可移植和安全的方式正确地完成任务,那么就没有理由使其不可移植和/或不安全。find命令提供了-exec选项,正是为了让用户能够迭代处理找到的路径名! - Kusalananda
@kusalananda:请提供一个单独的答案,以便我们可以为其投票 - rubo77
@rubo77 我刚刚已经做了。 - Kusalananda

你可以使用纯bash来实现,但最好使用find命令:
find . -maxdepth 1 -type d -exec echo {} \;

(此外,find 命令还将包括隐藏目录)

10请注意,此处不包括对目录的符号链接。您可以在bash中使用shopt -s dotglob来包含隐藏目录。您的命令还将包括.。另外请注意,-maxdepth不是一个标准选项(-prune 是)。 - Stéphane Chazelas
3dotglob选项很有趣,但是dotglob只适用于使用*find .命令总是会包括隐藏的目录(以及当前目录)。 - rubo77

这是为了在当前工作目录中查找可见和隐藏的目录,但不包括根目录而进行的操作:
只需循环遍历目录即可:
 find -path './*' -prune -type d

要在结果中包含符号链接:
find -L -path './*' -prune -type d

对每个目录(不包括符号链接)执行某项操作:
find -path './*' -prune -type d -print0 | xargs -0 <cmds>

排除隐藏目录:

find -path './[^.]*' -prune -type d

执行返回值上的多个命令(一个非常牵强的例子):
find -path './[^.]*' -prune -type d -print0 | xargs -0 -I '{}' sh -c \
"printf 'first: %-40s' '{}'; printf 'second: %s\n' '{}'"

可以使用“bash -c”等,而不是“sh -c”。

如果在 'for d in echo /' 中的 echo '/' 包含了60,000个目录,会发生什么情况? - AsymLabs
你会遇到“文件太多”错误,但这个问题是可以解决的:如何解决Debian中的“打开文件太多”问题 - rubo77
谢谢您提供的链接。我的意思是最好避免这个问题。 - AsymLabs
使用-L而不是-maxdepth 1选项在所有符号链接上会引发错误:“文件名”符号链接层级太多。 - rubo77
+1 对于 xargs(从标准输入构建并执行命令行) - rubo77
1xargs的示例只适用于一部分目录名称(例如,带有空格或换行符的目录名称将无法正常工作)。如果您不知道确切的名称,最好使用... -print0 | xargs -0 ... - Anthon
如何将目录“.”从结果中排除? - rubo77
如果当前目录下没有以点开头的任何目录名称,你可以使用find * .... - Anthon
好的,所以我编辑了你的答案(这是必须的,如果你想对文件夹中的所有目录进行操作)。 - rubo77
@rubo77 感谢你的编辑 - 我试着根据你和Anthons的评论对整篇文章进行修订,并使其符合POSIX标准。 - AsymLabs
xargs在一个命令中只能执行一个操作,所以我认为您需要创建一个函数,然后在xargs中调用该函数。您能提供一个示例吗? - rubo77
1@rubo77添加了排除隐藏文件/目录和执行多个命令的方法 - 当然,这也可以通过脚本完成。 - AsymLabs
2在我的答案底部,我添加了一个示例,如何使用find * | while read file; do ...的函数对每个目录执行某些操作。 - rubo77
如果你不想过度设计这个问题,你可以尝试使用这样简单的方法:find -type d - Hopping Bunny

您可以使用一行代码和多个命令循环遍历所有目录,包括隐藏目录(以点开始)。
for name in */ .*/ ; do printf '%s is a directory\n' "$name"; done

如果您想要排除符号链接:
for name in *; do 
  if [ -d "$name" ] && [ ! -L "$name" ]; then
    printf '%s is a directory\n' "$name"
  fi 
done

注意:在中使用列表*/ .*/可以工作,但同时会显示文件夹...;而在中,它不会显示这些文件夹,但如果文件夹中没有隐藏文件,则会报错。
一个更干净的版本,将包括隐藏目录并排除 "../",可以使用 bash 中的 dotglob shell 选项实现:
shopt -s dotglob nullglob
for name in */ ; do printf '%s is a directory\n' "$name"; done

nullglob shell选项可以使模式在没有匹配项时完全消失(而不是保留未展开的状态)。在zsh shell中使用模式*(ND/);/使前面的*只匹配目录,ND使其表现得好像同时设置了nullglob和dotglob。
您可以使用以下方式取消设置dotglob和nullglob:
shopt -u dotglob nullglob

使用find命令配合-exec选项来遍历目录并调用执行选项中的函数
dosomething () {
  echo "doing something with $1"
}
export -f dosomething
find ./* -prune -type d -exec bash -c 'dosomething "$0"' {} \;

使用 shopt -s dotglobshopt -u dotglob 来包含/排除隐藏目录

nullglobdotglob shell选项在这里不会起任何作用,因为shell从不用于进行通配符匹配。如果没有指定搜索路径,-path './*'测试对所有路径名都为真,因为它们都在.目录下(GNU find的默认搜索路径)。 - Kusalananda
谢了,我把-path移除了。 - rubo77
好,现在它将在当前目录中的一个名为“*”的目录中查找(由于引号)。去掉单引号将使其在当前目录中搜索每个非隐藏名称。你可能只想要使用“.”代替。 - Kusalananda
1谢谢,我去掉了单引号。我认为现在dotglob选项会生效。 - rubo77

仅列出目录。如果需要,在名称中包括空格和隐藏目录-A

grep '/$' <(ls -AF)

可能的续集:
| xargs -I{} echo {}

| while read; do echo $REPLY; done

但从这个棘手的问题来看,它指的是通过shell在for循环中输出目录。
for i in */ .[^.]*/; do
        [ -h "${i%?}" ] || echo $i
done

你能解释一下 [ -h "${i%?}" ] 吗? - rubo77
? - 匹配任意单个字符。类似于${i%/}。去除末尾的斜杠 - 目录指示符。如果没有这个操作,test 命令中的 -h-L 选项将无法正常工作。 - nezabudka
那你为什么不用斜杠呢?你还想在最后去掉什么? - rubo77
这只是两个等效选项之一,因为只能在末尾加上一个斜杠。这是我脑海中首先想到的事情。 - nezabudka

这个答案假设需要做的只是找到某个顶级目录的子目录。
对于一个特定于bash(以及在某种程度上适用于zsh shell)的答案,请参考rubo77's wiki answer的第二部分,其中使用dotglob和nullglob来正确地循环遍历单个目录中的目录(或目录的符号链接)。
对于一个不依赖于bash来进行实际文件选择的可移植解决方案,您可以使用find命令来循环遍历某个顶级目录中的所有目录,如下所示:
topdir=.

find "$topdir" ! -path "$topdir" -prune -type d -exec sh -c '
    for dirpath do
        # user code goes here, using "$dirpath"
        printf "\"%s\" is a directory\n" "$dirpath"
    done' sh {} +

上述代码假设我们感兴趣的顶级目录是当前目录(.)。将topdir设置为其他路径名以在另一个目录上使用它。 find实用程序被要求忽略与起始搜索路径相同的路径名! -path "$topdir",然后剪除(不进入)找到的任何其他路径名。这将限制搜索范围仅在由$topdir引用的目录中。
收集目录路径名(-type d),并使用批处理作为参数执行sh -c脚本。sh -c脚本是我们希望对所有目录执行的代码,因此它遍历给定的参数并对每个参数执行所需的操作。如果需要使用bash执行某些操作,则显然应该使用bash -csh -c脚本可以单独分离成自己的脚本,如下所示:
#!/bin/sh

for dirpath do
    # user code goes here, using "$dirpath"
    printf '"%s" is a directory\n' "$dirpath"
done

... 这将会从 find 被调用,如下所示:
find "$topdir" ! -path "$topdir" -prune -type d -exec ./myscript.sh {} +

另请参阅:

你可以使用 find "$topdir"/* 而不需要 ! -path "$topdir" - rubo77
@rubo77 这样做会使它搜索与 "$topdir"/* 匹配的所有路径名。这个模式不会匹配 $topdir 下的隐藏名称,因此也不会在该目录中找到任何隐藏的子目录。 - Kusalananda