3

I've got a folder with a load of folders in folders in folders etc... Some of the folders have files, and some do not. I want to cleanup the main folder by finding all directories with no files and deleting them. An example might make more sense:

So if I start with this:

  • mainFolder
    • folder1
      • folder1 (empty)
      • folder2
        • file.txt
      • folder3 (empty)
    • folder2
      • folder1 (empty)
      • folder2 (empty)
      • folder3
        • folder1
          • folder1 (empty)
    • folder3
      • folder1
        • file.txt

I should end up with this:

  • mainFolder
    • folder1
      • folder2
        • file.txt
    • folder3
      • folder1
        • file.txt

So:

  • /mainFolder/folder1/folder1 was deleted cause it had no files
  • /mainFolder/folder1/folder3 was deleted cause it had no files
  • /mainFolder/folder2 was deleted because cause it had no files, even all the sub-folders were empty

I hope this makes sense...

The only idea I had was to start at mainFolder and recursively travel down each sub-folder deleting the ones that are empty.

intelfx
  • 5,365
IMTheNachoMan
  • 419
  • 1
  • 7
  • 18

3 Answers3

10

See if this does what you want:

find mainFolder -depth -empty -type d -exec rmdir {} \;

That should find directories in mainFolder using a depth-first traversal that are empty, and remove those directories. Since it does a depth-first traversal, as it remove subdirectories, if the parent directory becomes empty, find will identify it as empty and remove it as well.

Andy Dalton
  • 13,993
  • 2
    Is there a reason you don't use -delete in place of -exec rmdir {} \;? In addition to being simpler, there is likely a big speed difference: -delete is built-in to find while rmdir is an external executable. – John1024 Oct 08 '17 at 05:09
  • 1
    @John1024 One reason is that it's a non-standard extension to find. But so is -empty... – Kusalananda Oct 08 '17 at 09:08
  • Well that worked brilliantly! Anyway to make it not delete the mainFolder if it is also empty? – IMTheNachoMan Oct 08 '17 at 12:43
  • 1
    @John1024 I didn't know about -delete, that's a good point -- thanks. – Andy Dalton Oct 08 '17 at 17:10
  • Note: Kusalananda's in-depth answer incorporates all of the above concerns (e.g., efficiency, non-standard extensions) by providing two equivalent find commands – one maximizing efficiency and the other maximizing portability. In short, it should have been accepted instead. </mournful_sigh> – Cecil Curry Jan 11 '19 at 04:03
3

Using an implementation of find that supports both -delete and -empty:

find mainFolder -type d -empty -delete

This will do a depth-first traversal of the directory structure rooted at mainFolder and delete any empty directories in there.

Using standard find:

find mainFolder -depth -type d -exec sh -c 'rmdir "$1" 2>/dev/null' sh {} ';'

This will attempt to use rmdir on every directory under mainFolder while traversing the structure in a depth-first manner. Since rmdir can't delete non-empty directories, only the empty ones will be deleted. Errors from the rmdir are discarded.

To explicitly test each directory before running rmdir over it:

find mainFolder -depth -type d -exec sh -c 'd="$1"; set -- "$d"/*; [ ! -e "$1" ] && rmdir "$d"' sh {} ';'

This assumes that there are no hidden files in the directories though.

Kusalananda
  • 333,661
  • @JdeBP That Perl code is exactly equivalent to the second find in my answer. – Kusalananda Oct 08 '17 at 09:23
  • No, it isn't. It doesn't fork extra processes and exec other process images. There is significant overhead in that if all that one wants to actually do is just execute one system call on each thing found. Not every task is a shell scripting task, and there is a reason that we have more languages than only shell script available. A good answer here should explore not only Python, as in another answer, but also the easily accessible one-liners in other languages. There's a tcl answer, too, for example. Recursive glob to feed into file delete is non-trivial compared to Perl, though. – JdeBP Oct 08 '17 at 09:48
  • 1
    @JdeBP It is functionally equivalent. If you're happy with doing directory manipulation in Perl, Python, Ruby, or C, and think that this is to be preferred on grounds of efficiency, then by all means do that. – Kusalananda Oct 08 '17 at 10:22
1

Solution

There is probably some switch combination to find to do this, but I think the Python approach is easier (and it's cross-platform, not that you need that):

import os

top = './mainFolder'
for root, dirs, files in os.walk(top, topdown=False):
    for name in dirs:
        dir_path = os.path.join(root, name)
        if not os.listdir(dir_path):  # An empty list is False
            os.rmdir(os.path.join(root, name))

Setup and Tests

Create the file directory

# https://stackoverflow.com/a/246128/295807
readonly script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd "$script_dir"

rm -rf mainFolder

mkdir -p mainFolder/{folder1/{folder1,folder2,folder3},folder2/{folder1,folder2,folder3/folder1/folder1},folder3/folder1}

touch mainFolder/folder1/folder2/file.txt
touch mainFolder/folder3/folder1/file.txt

Test:

$ tree mainFolder/
mainFolder/
├── folder1
│   ├── folder1
│   ├── folder2
│   │   └── file.txt
│   └── folder3
├── folder2
│   ├── folder1
│   ├── folder2
│   └── folder3
│       └── folder1
│           └── folder1
└── folder3
    └── folder1
        └── file.txt

12 directories, 2 files

Run the Python script:

python work.py

Test:

$ tree mainFolder/
mainFolder/
├── folder1
│   └── folder2
│       └── file.txt
└── folder3
    └── folder1
        └── file.txt

4 directories, 2 files

Note: if a file gets added to the folder after Python checks whether it's empty but before it actually erases it, Python will raise an exception! In practice, this isn't a big deal because the time between those two steps is measured in microseconds, but it is something to be aware of.

Ben
  • 240
  • 1
  • 10