Find all directories, that don't contain other directories

Viewed 224

Currently:

$ find -type d
./a
./a/sub
./b
./b/sub
./b/sub/dub
./c/sub
./c/bub

I need:

$ find -type d -not -contains -type d
./a/sub
./b/sub/dub
./c/sub
./c/bub

How do I exclude directories, that contain other (sub)directories, but are not empty (contain files)?

2 Answers

You can find the leaf directories that only have 2 links (or less) and then check if each found directory contains some files.

Something like this:

# find leaf directories
find -type d -links -3 -print0 | while read -d '' dir
do
    # check if it contains some files
    if ls -1qA "$dir" | grep -q .
    then
        echo "$dir"
    fi
done

Or simply:

find -type d -links -3 ! -empty

Note that you may need the find option -noleaf on some filesystems, like CD-ROM or some MS-DOS filesystems. It works without it in WSL2 though.

In the filesystem the directories always have 1 link so using -links won't work there.

A much slower, but filesystem agnostic, find based version:

prev='///' # some impossible dir

# A depth first find to collect non-empty directories
readarray -d '' dirs < <(find -depth -type d ! -empty -print0)

for dir in "${dirs[@]}"
do
    dirterm=$dir'/'

    # skip if it matches the previous dir
    [[ $dirterm == ${prev:0:${#dirterm}} ]] && continue

    # skip if it has sub directories
    [[ $(find "$dir" -mindepth 1 -maxdepth 1 -type d -print -quit) != '' ]] && continue

    echo "$dir"
    prev=$dir
done # add "| sort" if you want the same order as a "find" without "-depth"

You didn't show us which of these directories do and do not contain files. You specify files, so I'm working on the assumption that you only want directories that have no subdirectories but do have files.

shopt -s dotglob nullglob globstar     # customize glob evaluation
for d in **/                           # loop directories only
do for s in "${d}"*/                   # check subdirs in each
   do [[ -h "$s" ]] || continue 2      # skip dirs with subdirs
   done
   for f in "${d}"*                    # check for nondirs in each
   do echo "$d"                        # there's something here!
      continue 2                       # done with this dir, check next
   done
done

dotglob includes "hidden" files whose names start with a "dot" (.foo)
nullglob makes no*such return nothing instead of the string 'no*such'.
globstar makes **/ match arbitrary depth - e.g., ./x/, ./x/y/, and ./x/y/z/.

for d in **/ loops over all subdirectories, including subdirectories of subdirectories, though the trailing / means it will only report directories, not files.

for s in "${d}"*/ loops over all the subdirectories of $d if there are any. nullglob means if there are none, the loop won't execute at all. If we see a subdirectory, [[ -h "$s" ]] || continue 2 says if it entered this loop at all, symlinks are ok, but anything else disqualifies $d, so skip up 2 enclosing loops and advance the top level to the next dir.

if it gets this far, there are no invalidating real subdirectories, so we have to confirm there are files of some sort, even if they are just symlinks to other directories. for f in "${d}"* loops through anything else in the directory, since we know there aren't subdirs. It won't even enter the loop if the directory doesn't have something because of the nullglob, so if it goes in at all, anything there is a reason to report the dir (echo "$d") as non-empty. Once that's done, there's no reason to keep checking, so continue 2 again advances the top loop to the next dir to check!

I expected **/ to work, but it fails to get any subdirectories at all on my Windows/Git Bash emulation. **/*/ ignores subdirectories of the current directory, which is why I originally used */ **/*/, but **/ prevents redundancies when run on a proper Centos VM. Use that.

Related