Thomas G Henry LLC
Thomas G Henry LLC

Reputation: 11224

Use GNU find to show only the leaf directories

I'm trying to use GNU find to find only the directories that contain no other directories, but may or may not contain regular files.

My best guess so far has been:

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

but this just gets me a long list of "."

Thanks!

Upvotes: 63

Views: 24928

Answers (10)

ChrisoLosoph
ChrisoLosoph

Reputation: 617

EDIT: Thanks to @Walf for the remark in the comment of the opening post. find "$dir" -type d -empty works. It's so typical regarding Stack Overlow. Old posts make you miss the modern better solution. You can also find it in the manual.

If there is a version of find without -empty then the checked answer doesn't work on BTRFS, the filesystem that I use, and I am not satisfied with any other answer listed here. There is a simpler standard answer by using find without awk or sed.

find "$dir" -type d \( -exec sh -c '[ "$(/usr/bin/ls -A "{}")" ]' \; -o -print \)

The sh -c '…' invocation is only necessary for the command substitution $(…). If you know how to avoid the \( -o -print \) part, let me know.

The options to /usr/bin/ls can be used to refine the meaning of an empty directory. -A will also show hidden files and folders (except . and ..).

It's still somewhat long for that simple purpose but I hope this works on any filesystem. It might be worth to define a function for not repeating this command.

Upvotes: -1

Niloct
Niloct

Reputation: 10015

My 2 cents on this problem:

#!/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

It uses a subshell to capture output from the run, and lists directories which have files.

Upvotes: 0

raf
raf

Reputation: 53

There is an alternative to find called rawhide (rh) that is much easier to use.

For filesystems other than btrfs:

rh 'd && nlink == 2'

For btrfs:

rh 'd && "[ `rh -red %S | wc -l` = 0 ]".sh'

A shorter/faster version for btrfs is:

rh 'd && "[ -z \"`rh -red %S`\" ]".sh'

The above commands search for directories and then list their sub-directories and only match when there are none (the first by counting the number of lines of output, and the second by checking if there is any output at all per directory).

For a version that works on all filesystems as efficiently as possible:

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

On normal (non-btrfs) filesystems, this will work without the need for any additional processes for each directory, but on btrfs, it will need them. This is probably best if you have a mix of different filesystems including btrfs.

Rawhide (rh) is available from https://raf.org/rawhide or https://github.com/raforg/rawhide. It works at least on Linux, FreeBSD, OpenBSD, NetBSD, Solaris, macOS, and Cygwin.

Disclaimer: I am the current author of rawhide.

Upvotes: 1

Sylvain Defresne
Sylvain Defresne

Reputation: 44623

You can use -links if your filesystem is POSIX compliant (i.e. a directory has a link for each subdirectory in it, a link from its parent and a link to itself, thus a count of 2 links if it has no subdirectories).

The following command should do what you want:

find dir -type d -links 2

However, it does not seems to work on Mac OS X (as @Piotr mentioned). Here is another version that is slower, but does work on Mac OS X. It is based on his version, with a correction to handle whitespace in directory names:

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

Upvotes: 105

Daniel Gray
Daniel Gray

Reputation: 1802

This awk/sort pipe works a bit better than the one originally proposed in this answer, but is heavily based on it :) It will work more reliably regardless of whether the path contains regex special characters or not:

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

Remember that awk strings are 1-indexed instead of 0-indexed, which might be strange if you're used to working with C-based languages.

If the index of the current line in the previous line is 1 (i.e. it starts with it) then we skip it, which works just like the match of "^"$0.

Upvotes: 0

A.Ellett
A.Ellett

Reputation: 373

I have some oddly named files in my directory trees that confuse awk as in @AhmetAlpBalkan 's answer. So I took a slightly different approach

  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) 

As in the awk solution, I reverse sort. That way if the directory path is a subpath of the previous hit, you can easily discern this.

Here p is my previous match, c is the current match, l is the length of the current match, f is the first l matching characters of the previous match. I only echo those hits that don't match the beginning of the previous match.

The problem with the awk solution offered is that the matching of the beginning of the string seems to be confused if the path name contains things such as + in the name of some of the subdirectories. This caused awk to return a number of false positives for me.

Upvotes: 2

ahmet alp balkan
ahmet alp balkan

Reputation: 45312

I just found another solution to this that works on both Linux & macOS (without find -exec)!

It involves sort (twice) and awk:

find dir -type d | sort -r | awk 'a!~"^"$0{a=$0;print}' | sort

Explanation:

  1. sort the find output in reverse order

    • now you have subdirectories appear first, then their parents
  2. use awk to omit lines if the current line is a prefix of the previous line

    • (this command is from the answer here)
    • now you eliminated "all parent directories" (you're left with parent dirs)
  3. sort them (so it looks like the normal find output)
  4. Voila! Fast and portable.

Upvotes: 6

kenorb
kenorb

Reputation: 166919

Here is solution which works on Linux and OS X:

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

or:

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

Upvotes: 0

DREV
DREV

Reputation: 1

What about this one ? It's portable and it doesn't depend on finnicky linking counts. Note however that it's important to put root/folder without the trailing /.

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

Upvotes: 0

Piotr Czapla
Piotr Czapla

Reputation: 26582

@Sylvian solution didn't work for me on mac os x for some obscure reason. So I've came up with a bit more direct solution. Hope this will help someone:

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

Explanation:

  • ls -p ends directories with '/'
  • so (ls -p XXX | grep / >/dev/null) returns 0 if there is no directories
  • -print0 && -0 is to make xargs handle spaces in directory names

Upvotes: 3

Related Questions