List subdirectories one level down that do not contain a certain file

How can I find all subdirectories in a directory (only 1 level down) that do not contain a given file ?


There are several ways to accomplish this with find.

Approach 1 is less hacky and will work well with strange directory names (e.g., directory names containing newlines), but approach 2 should be faster if there are a lot of directories.

Approach 1

Command

find DIR -type d -mindepth 1 -maxdepth 1 -not -exec test -f {}/FILENAME \; \
    -print | sort

How it works

  • find DIR -type d -mindepth 1 -maxdepth 1 finds all directories (-type d) in DIR, with depth 1.

  • -not -exec test -f {}/FILENAME \; is true if and only if a file called FILENAME could not be found in the currently processed directory ({}).

  • -print will output the desired directory names.

  • If desired, sort will sort the output alphabetically.

Approach 2

Command

( find DIR -type f -mindepth 2 -maxdepth 2 -name FILENAME -printf "%h\n" ; \
    find DIR -type d -mindepth 1 -maxdepth 1 ) | sort | uniq -u

How it works

  • find DIR -type f -mindepth 2 -maxdepth 2 -name FILENAME finds all files (-type f) called FILENAME in the subdirectories of DIR (files of directories of depth 1 have depth 2).

  • -print "%h\n" print the names of the directories containing files named FILENAME, followed by a newline.

  • find DIR -type d -mindepth 1 -maxdepth 1 list all directories (-type d) in DIR, with depth 1.

  • sort sorts the output alphabetically (output must be sorted when piping to uniq).

  • uniq -u prints only unique lines.

    Every subdirectory of DIR gets listed at least once, but those that contain a file called FILENAME get listed twice. uniq -u eliminates the latter kind.


Directly in shell script:

for i in DIRECTORY/*/; do [ -f "$i/FILENAME" ] || basename "$i"; done
  • for i in DIRECTORY/*/ uses shell expansion to safely (shouldn't be any problems with strange directory names) give all sub directories of a specific directory. Note the trailing slash to only give the directories.
  • [ -f "$i/FILENAME" ] returns true if the file named FILENAME exists in the directory in this iteration. The || operator makes the following command run if the first one returned false, i.e. only if the file did not exist in the directory.
  • basename "$i" prints the directory name (if the file name wasn't found therein). If you want the full path and not just the directory name, substitute basename for echo or readlink -f or something else per preference.

If you also want to include hidden directories, run (in Bash)

shopt -s dotglob

before the command.