8

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?

2
  • 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

5

(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
            print;
        }
    }
}' | xargs echo rm -rf

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

4
  • 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
2

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

tree_contains_foo_files()
{
    # 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"
    fi
done

Given this test tree:

.
./dir1
./dir1/dir1.1
./dir1/dir1.1/dir1.1.1
./dir1/dir1.1/dir1.1.1/file.foo
./dir1/dir1.1/file.bar
./dir2
./dir2/dir2.1
./dir2/file.bar
./dir3

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.

3
  • 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
1

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 -- {} +
1

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~''

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .