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 touniq
).-
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 namedFILENAME
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, substitutebasename
forecho
orreadlink -f
or something else per preference.
If you also want to include hidden directories, run (in Bash)
shopt -s dotglob
before the command.