I have a few directories, some with a depth of 3, which contain mixed file types. What I need to do is to rm -rf all the subdirectories that do not contain filetype foo.

Is this achievable with find somehow? I do know that I can use find like this:

find . ! -name '*.foo' -delete

to delete all files within the directories that do not contain any file of type *.foo. Now, how can I use this, to not only delete all unwanted files, but all directories and subdirectories which do not contain *.foo?

  • So if you have /a/b, and b has no *.foo files, but /a/b/c has *.foo files, obviously we don't want to run rm -rf /a/b, right? In this case, I don't think rm -rf is the right approach, maybe we need something like rm dir/*; rmdir dir.
    – Mikel
    Mar 1, 2011 at 3:20
  • If a directory contains some.foo and some.bar, should it be deleted? Your question is not clear in this respect. Mar 1, 2011 at 23:31

4 Answers 4


(Your question is not clear: if a directory contains some.foo and some.bar, should it be deleted? I interpreted it as requiring such a directory to be kept.)

The following script should work, provided that no file name contains a newline and no directory matches *.foo. The principle is to traverse the directory from the leaves up (-depth), and as *.foo files are encountered, the containing directory and all parents are marked as protected. Any reached file that is not *.foo and not protected is a directory to be deleted. Because of the -depth traversal order, a directory is always reached after the *.foo files that might protect it. Warning: minimally tested, remove the echo at your own risk.

find . -depth -name '*.foo' -o -type d | awk '{
    if ($0 ~ /\.foo$/) {
        while (sub("/+[^/]+$", "")) {protect[$0]=1;}
    } else {
        if (!protect[$0]) {
            gsub("[\\\001-/]", "\\\\&"); # protect file names for xargs
}' | xargs echo rm -rf

For once, I'm not proposing a zsh solution.

  • 1
    Maybe File::Find would be the right tool for this job. Mar 1, 2011 at 23:27
  • I get an error on awk script line 5 running that, but it seems OK if I change [\\\011-/] to [\011-].
    – Mikel
    Mar 2, 2011 at 1:28
  • @Mikel: That's weird, I get no error under Gawk, Mawk or the original awk, and I don't see what could be wrong. Your regexp "[\001-]" (I assume \011 is a typo) matches only \001 and -, which is no good since the point is to protect \\'" and whitespace. Mar 2, 2011 at 7:58
  • Yes, sorry, I meant \001.
    – Mikel
    Mar 2, 2011 at 8:53

I'm not quite sure this can be done using only find, but I think we can do it using only bash and find.

    # return true (0) as soon as we find a "*.foo" file
    find "$1" -type f -name "*.foo" -print0 |
        read -r -d $'\0' file && return 0

    return 1

find . -depth -type d -print0 |
while read -r -d $'\0' dir; do
    if ! tree_contains_foo_files "$dir"; then
        rm -rf "$dir"

Given this test tree:


I get this result:

rm -rf ./dir2/dir2.1
rm -rf ./dir2
rm -rf ./dir3

which I think is what you want, i.e. don't delete dir1, because dir1/dir1.1/dir1.1.1 contains file.foo.

But note that it does process directories multiple times, so it might be slow for large trees. If efficiency is important, I'd use a more powerful programming language.

  • 1
    There are more efficient implementations of tree_contains_foo_files, such as [ -n "$(find … | head -c 1)" ], or (faster but requires GNU find) [ -n "$(find … -printf a -quit)" ]. Mar 1, 2011 at 23:34
  • Thanks for the suggestion. You're right I should short-circuit as soon as a file is found. Seeing as I'm using read already, how about find ... | read file && return 0. I think that is very slightly faster again.
    – Mikel
    Mar 2, 2011 at 1:21
  • Or of course find ... | read -n 1 ....
    – Mikel
    Mar 2, 2011 at 1:31

IIUYC, you can simply first remove all unwanted files using

find . ! -type f -name '*.foo' -delete

which may empty some directories. Then you can remove the empty directories (and directories containing only empty directories, etc.) like in my question

find . -depth -mindepth 1 -empty -type d -exec rmdir -p -- {} +

If more than just find is allowed, a much simpler solution is:

find -type d -not -path . | while IFS='' read -r path; do [[ ! -e "$path/$notThisFile" ]] && echo "$path"; done

Replace -e with any test you like.

This should print you all sub-directories that don’t contain $notThisFile.

Verbose example, where all non-hidden sub-directories that contain the file are highlighted:

find -type d -not -path . | while IFS='' read -r path; do
  [[ ! -e "$path/$notThisFile" ]] && echo "$path"
done | sed -r 's~^([^.]*)/'"$notThisFile"'$~\033[1;37m\1\033[0m~''

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.