使用GNU find仅显示叶子目录。

60

我试图使用GNU find仅查找包含其他目录的目录,但可能包含或不包含常规文件。

到目前为止,我最好的猜测是:

find dir -type d \( -not -exec ls -dA ';' \)

但这只会给我一个很长的“。”列表。

谢谢!


1
当使用-exec时,{}参数会扩展为当前检查的文件系统对象(文件/目录/...)的路径。因此,您应该使用以下命令来打印目录:find dir -type d \\( -not -exec ls -dA {} \; \\) - Sylvain Defresne
2
同样的问题在Super User上:使用“find”列出仅包含没有更多子目录的目录 - Gilles 'SO- stop being evil'
参见:在Linux中列出所有叶子子目录 - Dennis Williamson
1
由于这个问题在搜索中排名很高,请参见 https://dev59.com/l2ox5IYBdhLWcg3wIQ1o#9418016,该答案提供了最简单的解决方案:`find -type d -empty`。 - Walf
9个回答

102

如果你的文件系统符合 POSIX 标准(即每个子目录都有一个链接,父目录和自身也各有一个链接,因此如果没有子目录,则链接数为 2),则可以使用 -links

以下命令应该能满足你的需要:

find dir -type d -links 2

然而,在 Mac OS X 上似乎不起作用(如 @Piotr 所述)。这里是另一个版本,虽然速度较慢,但在 Mac OS X 上可以使用。它基于他的版本,对处理目录名称中的空格进行了更正:

find . -type d -exec sh -c '(ls -p "{}"|grep />/dev/null)||echo "{}"' \;

@SylvainDefresne,你知道它是否能在通过NFS挂载的NetApp文件系统上运行吗? - oz123
2
同样地,简单的解决方案似乎在Cygwin(Windows 7)中无法工作,但扩展的OSx版本可以。 - Eric B.
2
在我的Btrfs系统中,目录的链接计数为1,因此这无法工作。 - miguel.negrao
即使在2019年,Mac仍未修复此问题。 - Sridhar Sarnobat
不幸的是,这在NTFS磁盘上无法工作。第一个解决方案使用-links 2也不行,第二个方案也不行,因为有一些目录的名称中带有$符号,例如$RECYCLE.BIN。但是在ext[234]分区上可以正常运行。 - mivk
显示剩余4条评论

6
我刚找到了另一种适用于Linux和macOS(不需要使用find -exec)的解决方法!它涉及两次使用sortawk:
find dir -type d | sort -r | awk 'a!~"^"$0{a=$0;print}' | sort

说明:

  1. find命令的输出结果按照相反顺序排序。

    • 这样,子目录将先于它们的父目录出现。
  2. 使用awk命令在当前行是前一行的前缀时省略行。

    • (该命令来自这里的答案)
    • 现在,您已经排除了“所有父目录”(只剩下父目录)。
  3. sort它们(使其看起来像正常的find输出)。
  4. 完成!快速且可移植。

这个巧妙/便携的答案唯一的问题是,正如这里所指出的那样,如果文件夹名称中的任何字符都是正则表达式特殊字符,则会失败。我进行了小修改并在这里发布了我的答案。 - Daniel Gray
如果一个目录的子字符串与另一个目录的子字符串相同,这种方法将无法正常工作。例如,如果一个叶子目录名为“foo”,另一个目录名为“foobar”,那么这个方法只会显示“foobar”。 - Chris Down
就此而言,您可以使用sed在awk之前将“/”附加到每行末尾,然后在awk之后将其删除。 - Nathaniel_Wu

3

对于某些晦涩的原因,@Sylvian的解决方案在我的mac os x上没有起作用。因此,我想出了一个更直接的解决方案。希望这能帮到某些人:

find . -type d  -print0 | xargs -0 -IXXX sh -c '(ls -p XXX | grep / >/dev/null) || echo XXX' ;

解释:

  • ls -p命令会在目录名后面加上 '/' 符号
  • 因此,(ls -p XXX | grep / >/dev/null) 命令如果没有目录存在,则返回 0
  • -print0-0 的作用是让 xargs 命令处理目录名中的空格

困惑。在 MacOS 上,find -print0xargs -0 也不是开箱即用的;但是当然,你可以通过 find -exec 避免它们,就像 Sylvain 的更新答案所演示的那样。 - tripleee
我喜欢这个解决方案。看起来非常易读,是在“links 2”方法无法工作的情况下的一个很好的选择。但是我确实需要对“XXX”加上双引号。 - user1593842

2

我在目录树中有一些奇怪命名的文件,这让 awk 产生了困扰,就像 @AhmetAlpBalkan 的答案所述。因此,我采用了稍微不同的方法。

  p=;
  while read c;
    do 
      l=${#c};
      f=${p:0:$l};
      if [ "$f" != "$c" ]; then 
        echo $c; 
      fi;
      p=$c; 
    done < <(find . -type d | sort -r) 

与awk解决方案一样,我进行反向排序。这样,如果目录路径是前一个命中的子路径,您可以轻松地辨别出来。
这里,p是我的上一个匹配,c是当前匹配,l是当前匹配的长度,f是前一个匹配的前l个匹配字符。我只回显不与前一个匹配的开头匹配的命中内容。
提供的awk解决方案的问题在于,如果路径名包含诸如+之类的子目录名称,那么字符串开头的匹配似乎会混淆。这导致awk为我返回了许多错误结果。

1
在处理文件时,请引用您的变量,否则会出现错误。 - Walf

1

有一个名为Rawhide(RH)的替代工具,比find命令更易于使用。

对于除btrfs以外的文件系统:

rh 'd && nlink == 2'
btrfs:
rh 'd && "[ `rh -red %S | wc -l` = 0 ]".sh'
btrfs的更短/更快版本如下:
rh 'd && "[ -z \"`rh -red %S`\" ]".sh'

以上命令搜索目录,列出它们的子目录,并仅在没有匹配项时进行匹配(第一个通过计算输出行数,第二个通过检查每个目录是否有任何输出来实现)。

要在所有文件系统上以尽可能高效的方式工作的版本:

rh 'd && (nlink == 2 || nlink == 1 && "[ -z \"`rh -red %S`\" ]".sh)'

在普通(非btrfs)文件系统上,这将无需任何额外的进程就能正常工作,但在btrfs上,则需要它们。如果您有包含btrfs在内的不同文件系统混合使用,那么这可能是最好的选择。
Rawhide(rh)可以从 https://raf.org/rawhidehttps://github.com/raforg/rawhide 获取。它至少适用于Linux、FreeBSD、OpenBSD、NetBSD、Solaris、macOS和Cygwin。
免责声明:我是rawhide的现任作者。

“wc -l” 变体看起来有些可疑;也许可以参考“无用的 wc 用法”。(https://www.iki.fi/era/unix/award.html#wc) - tripleee
可以避免使用wc。这就是第二个版本所展示的。Shell不再使用wc计算rh输出的行数并将其与零进行比较,而是仅测量任何rh输出的长度。 - raf

0

这个怎么样?它是便携式的,不依赖于棘手的链接计数。但请注意,重要的是将root/folder放在末尾没有/。

find root/folder -type d | awk '{ if (length($0)<length(prev) || substr($0,1,length(prev))!=prev) print prev; prev=($0 "/") } END { print prev }'

0

关于这个问题,我的两点看法:

#!/bin/bash
(
while IFS= read -r -d $'\0' directory
do
    files=$(ls -A "$directory" | wc -l)
    if test $files -gt 0 
    then
        echo "$directory"
    fi
done < <(find . -type d -print0)
) | sort | uniq

它使用子Shell来捕获运行结果,并列出其中有文件的目录。

我实际上不认为子shell是必要或有用的。find命令中的-print0选项是GNU扩展,不太可移植。在脚本中使用ls命令始终是可疑的。 - tripleee
这个内容来源于 https://mywiki.wooledge.org/BashFAQ/020。 - Niloct
需要使用子shell来捕获输出,否则所有目录都会被列出并打印。 - Niloct
done 重定向到管道中就足够了。 - tripleee

0

下面是一个适用于Linux和OS X的解决方案:

find . -type d -execdir bash -c '[ "$(find {} -mindepth 1 -type d)" ] || echo $PWD/{}' \; 

或者:

find . -type d -execdir sh -c 'test -z "$(find "{}" -mindepth 1 -type d)" && echo $PWD/{}' \;

那不应该是“-maxdepth”吗? - Tom Hale

0

这个 awk/sort 管道比最初提出的 在这个答案中 更好,但是它严重依赖于它 :) 它将更可靠地工作,无论路径是否包含正则表达式特殊字符:

find . -type d | sort -r | awk 'index(a,$0)!=1{a=$0;print}' | sort

请记住,awk字符串是从1开始索引而不是从0开始索引的,如果您习惯于使用基于C的语言,则可能会感到奇怪。

如果当前行的索引在上一行中是1(即以它开头),那么我们跳过它,这就像匹配"^"$0一样。


这将无法匹配名称为同级目录前缀的目录。例如,如果您有路径 /a/a/a/ab,则不会报告 /a/a - Ruud
使用find命令的-depth选项如下:find . -depth -type d | awk 'index(a,$0)!=1{a=$0;print}' - Chubler_XL
这显然会在包含换行符的目录名上失败。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接