TechEarl

How to Find (and Delete) Empty Directories and Files

find . -type d -empty lists every empty directory; find . -type f -empty lists every empty file. The catch is what 'empty' means (a hidden file makes a directory not empty) and the -depth trap that lets find -delete collapse whole nested empty trees in one pass. The flag reference, the safe two-pass cleanup, the BSD vs GNU notes, and the mistakes that bite.

Ishan Karunaratne⏱️ 13 min readUpdated
Share thisCopied
Use find -type d -empty to list empty directories and find -type f -empty for empty files. The -depth trap for deleting nested empty trees, the hidden-file gotcha, the safe two-pass cleanup, and BSD vs GNU find notes.

find . -type d -empty lists every empty directory under the current path. Swap -type d for -type f and you get every empty file instead. Both rely on the -empty test, which matches anything with nothing in it: zero entries for a directory, zero bytes for a file.

The subtlety that catches people is what "empty" means, and the order find walks the tree when you add -delete. An empty directory stops being empty the moment it holds a single hidden file or one subdirectory, even an empty one. And find -delete collapses whole nested empty trees in a single pass only because of a flag most people forget. This page is the reference I keep open when I write disk-cleanup and post-build scripts.

Set your values

Try it with your own values

Set your OS and search path. Every find example below updates with your values.

The one-liner

Empty directories:

bash· Linux (GNU)
find :search_path -type d -empty

Empty files:

bash· Linux (GNU)
find :search_path -type f -empty

-type d restricts the match to directories, -type f to regular files, and -empty does the actual emptiness test. Drop the -type filter and find :search_path -empty returns both at once, which is occasionally what you want for a raw inventory but rarely what you want before a delete.

What "empty" actually means

For a file, empty is simple: zero bytes. A file with a single newline or a single space is not empty; it has content.

For a directory, empty means zero entries. This is where the surprises live:

  • A directory that contains one subdirectory is not empty, even if that subdirectory is itself empty. The subdirectory counts as an entry.
  • A directory that contains one hidden file (.gitkeep, .DS_Store, .env) is not empty. -empty does not skip dotfiles; a hidden entry is still an entry.
  • A directory that contains a broken symlink is not empty. The link is an entry regardless of whether its target exists.

That last point about hidden files is the single most common reason find -type d -empty returns fewer directories than someone expects. A folder that looks empty in Finder or in a plain ls can be holding a .DS_Store that macOS dropped there, and find correctly reports it as non-empty. Run ls -la (note the -a) on a directory before you assume find is wrong.

Delete empty directories

The cleanup form adds -delete, and it needs one more flag to behave:

bash· Linux (GNU)
find :search_path -depth -type d -empty -delete

The -depth flag is doing real work here, and leaving it off is the most common mistake in this whole topic.

By default find processes a directory before its contents (pre-order traversal). With -depth, it processes the contents first and the directory itself last (post-order, depth-first). That ordering matters for -delete because of a chain reaction.

Picture a/b/c where c is empty, b contains only c, and a contains only b. Without -depth, find visits a first; at that moment a is not empty (it holds b), so a is skipped. It then visits b (not empty, holds c) and skips it, then visits c, finds it empty, and deletes it. Result: only c is gone, a and b survive even though the entire tree was effectively empty.

With -depth, find visits c first and deletes it. Now b is empty, so when find reaches b (after its children), b is deleted too. Then a becomes empty and gets deleted. The whole nested tree collapses in one pass.

find -delete implies -depth automatically in modern GNU and BSD find, so the bare find . -type d -empty -delete often works. But the -empty test is evaluated as find walks the tree, and whether it sees the post-deletion state depends on traversal order being applied before the test runs. Writing -depth explicitly, before the other tests, removes all doubt and makes the intent obvious to whoever reads the script next. I always write it.

As with any destructive find, dry-run first. Swap -delete for -print (or -print plus -depth to see the exact order), eyeball the list, then put -delete back. There is no confirmation and no undo. The find and delete files article covers the safe-delete pattern in more depth.

The two-pass cleanup

A directory full of empty files is not itself empty, so a single -type d -empty -delete pass will not touch it. The standard fix is two passes: delete the empty files first, then delete the directories that the first pass just emptied out.

bash· Linux (GNU)
find :search_path -type f -empty -delete
find :search_path -depth -type d -empty -delete

Pass one removes every zero-byte file. Pass two then sees directories that held only those files as genuinely empty and removes them, with -depth collapsing any nested chains. The order is not optional: run the directory pass first and it skips folders that the file pass would have emptied, so you would have to run it again.

This is the pattern I reach for after a failed export, a half-finished rsync, or a build that scattered placeholder files. One run, two lines, and the stale skeleton is gone.

Find empty directories, excluding some paths

When the search path contains a subtree you never want to touch (node_modules, .git, a mounted backup), filter it out with -not -path:

bash· Linux (GNU)
find :search_path -type d -empty -not -path '*/node_modules/*' -not -path '*/.git/*'

-not -path PATTERN (you can also write it ! -path PATTERN) drops any path matching the glob. The */ prefix and /* suffix make it match the directory at any depth. Chain as many -not -path clauses as you need.

For large trees there is a faster idiom: -prune stops find from descending into a subtree at all, rather than visiting every entry and discarding it. find :search_path -path '*/node_modules' -prune -o -type d -empty -print skips node_modules wholesale. -prune is the better choice when the excluded subtree is huge; -not -path is simpler when it is small. The find command cheat sheet has the full -prune pattern.

Count empty directories

Pipe the listing into wc -l to get a number instead of a list:

bash· Linux (GNU)
find :search_path -type d -empty | wc -l

wc -l counts newlines, so each matched path adds one to the total. The caveat that applies to every find | wc -l: a directory name containing a literal newline would be counted twice. That is rare enough to ignore for a quick disk audit, but if you are counting paths you do not control, the safe form is find :search_path -type d -empty -printf '.' | wc -c on GNU (BSD find has no -printf).

macOS BSD vs GNU find

The -empty test is one of the well-behaved corners of find. Both implementations support the flags this article uses, with the same meaning:

BehaviorGNU findBSD find (macOS default)
-empty testSupportedSupported
-depth (post-order traversal)SupportedSupported
-delete actionSupported (implies -depth)Supported (implies -depth)
-not / ! operatorSupportedSupported
-path testSupportedSupported
-prune actionSupportedSupported
-printf (for safe counting)SupportedNOT supported; use -exec

So the empty-directory and empty-file workflow is portable: a script written with -empty, -depth, -delete, -not -path, and -prune runs unchanged on both Linux and macOS. The only gap is -printf, which you only need for the newline-safe count. If a cleanup script has to run on both platforms, stick to that intersection and you will not get bitten.

Common mistakes

1. Forgetting -depth when deleting directories. Without it, find evaluates a parent directory before deleting its children, sees the parent as non-empty, and skips it. Nested empty trees survive except for the innermost leaf. find -delete implies -depth, but write -depth explicitly so the intent is unambiguous and the command is correct even if you later swap -delete for -exec rmdir.

2. The hidden-file gotcha. A directory holding a single .gitkeep, .DS_Store, or .env is not empty, and find -type d -empty correctly skips it. People see a "blank" folder in a file browser, run the command, get nothing, and assume find is broken. It is not. Run ls -la on the directory to see the dotfile.

3. Treating a directory of empty files as empty. A folder containing zero-byte files is itself non-empty; it has entries. A single -type d -empty pass will not remove it. Use the two-pass cleanup: empty files first, then empty directories.

4. Running it against /. find / -type d -empty -delete is a foot-cannon. It will walk the entire filesystem, including /proc, /sys, and every mounted volume, and happily delete empty directories that the OS or other software depends on. Always anchor the search at a real project or scratch directory, and add -xdev if you want to stay on one filesystem.

5. Skipping the dry run. find -delete has no confirmation prompt and no undo. Always run the command with -print first, read the list, then swap in -delete. This costs two seconds and has saved me from deleting directories I forgot were load-bearing.

6. Putting -depth after the tests. -depth is a global option; placing it after -type d is technically legal but reads as if it were a test. Convention, and clarity, put it right after the search path: find :search_path -depth -type d -empty -delete.

When NOT to use this

find -empty is the right tool for cleaning up structural cruft, but reach for something else when:

  • You want to free disk space. Empty directories take essentially no space. If the goal is reclaiming gigabytes, you want du -sh */ | sort -h or find files larger than a size, not an empty-directory sweep.
  • The "empty" directories hold intentional placeholders. Many repos use .gitkeep to keep an otherwise-empty directory tracked in Git. Those are not empty (the dotfile is an entry), so find -type d -empty skips them correctly. But if you have a habit of using truly empty directories as markers, a blanket delete will wipe your markers. Know your tree.
  • You need the directories gone in a specific order with logging. find -delete is silent and order is fixed by -depth. If you need to log each removal, handle errors per directory, or delete conditionally, loop with -exec rmdir {} \; or a bash while loop over -print0 output instead.
  • Another process is writing into the tree. find walking a directory while a build or an upload is still creating files in it will produce a stale snapshot, and a delete pass can race the writer. Run cleanup only when nothing else is touching the path.

See also

FAQ

TagsfindCLILinuxmacOSBSDCleanup ScriptsDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

find -delete removes every matched file with no confirmation. The safe -print-first dry-run pattern, depth-first directory deletion, when to use -exec rm vs xargs rm -f, and the BSD vs GNU differences.

How to Find and Delete Files Safely with find -delete

find -delete removes every matched file with no confirmation and no undo. The safe pattern is to write the command with -print first, eyeball the list, then swap -print for -delete. Plus the directory-depth-first trap, when to use -exec rm instead, and the find -delete vs xargs rm -f tradeoff.

Skip node_modules, .git, and build output from grep with --exclude-dir, exclude filename globs with --exclude, and search only matching files with --include. The one-liner, brace expansion, --exclude-from, and the BSD grep fallback.

How to Exclude Files and Directories from grep

grep does not read .gitignore, so skipping node_modules, .git, and build output is on you. The flags that do it: --exclude for filename globs, --exclude-dir for whole directories, --include for the inverse, --exclude-from to read the list from a file, plus the find -prune fallback for older macOS grep.