Removing Files and Directories (rm, rmdir)

The rm command deletes files, and rmdir deletes empty directories — together they are how you clean up a Linux filesystem from the command line. Unlike a desktop trash can, there is no built-in undo: once a file is unlinked and no running process still has it open, its data blocks become eligible to be overwritten and the file is effectively gone. Understanding exactly what rm does under the hood, and building the habit of previewing before you delete, is one of the most important defensive skills you will learn on the command line.

Overview / How it works

Every file on a Linux filesystem is really two things: a directory entry (a name, stored in its parent directory) and an inode, a data structure holding the file’s metadata — owner, permissions, size, timestamps, and pointers to the actual data blocks on disk. The name you see in ls is just a label pointing at an inode; it is entirely possible for one inode to have several names pointing at it at once, through hard links. Every inode keeps a link count: the number of directory entries currently pointing at it.

When you run rm somefile, the command does not directly erase data. It calls the unlink() system call, which removes the directory entry and decrements the inode’s link count by one. Only when that link count reaches zero, and no running process still holds the file open via a file descriptor, does the kernel actually reclaim the data blocks and mark the inode free. This is why a classic sysadmin gotcha exists: if a long-running process (say, a web server) has a huge log file open and you rm the file to free disk space, df may still report the disk as full — the data blocks are not released until the process closes the file or is restarted. The fix in that situation is usually to truncate the file in place (: > /var/log/app.log) rather than delete it out from under the process.

Directories are handled differently because a directory is not just data — it contains entries for other files, plus the special . and .. entries pointing at itself and its parent. The kernel will not let you unlink a directory the same way as a file, because that could leave dangling entries elsewhere in the tree. This is exactly why plain rm refuses a directory outright, and why rmdir only ever succeeds on a directory that is completely empty (containing nothing but . and ..). To remove a directory that still has contents, rm needs the -r (recursive) flag, which walks the tree depth-first, unlinking every file it finds, then removes each now-empty directory on the way back up.

There is no recycle bin here. GUI file managers implement trash themselves, usually by moving files into a hidden folder instead of calling unlink() directly — the command line has no such layer by default. Treat every rm as final unless you have backups, snapshots, or a dedicated trash utility in place (more on that in Best Practices).

Syntax

rm [OPTION]... FILE...
rmdir [OPTION]... DIRECTORY...

Both accept one or more paths, and options can be combined (rm -rf is -r plus -f).

rm option Meaning
-i Prompt for confirmation before every removal
-I Prompt only once, before removing more than three files or before a recursive removal — less intrusive than -i
-f Force: ignore nonexistent files, never prompt, suppress most error messages
-r, -R Recursive: remove directories and everything inside them
-d Remove empty directories too (like rmdir, but via rm)
-v Verbose: print each file as it is removed
--preserve-root Default behavior: refuse to operate recursively on /
--no-preserve-root Disable that safeguard — extremely dangerous, almost never a good idea
rmdir option Meaning
-p Also remove each empty parent directory in the given path
-v Verbose: print a message for each directory processed
--ignore-fail-on-non-empty Suppress the error for non-empty directories (they still are not deleted)

Examples

Example 1: Removing a single file

ls ~/downloads/old-invoice.pdf
rm ~/downloads/old-invoice.pdf
ls ~/downloads/old-invoice.pdf

Output:

/home/alex/downloads/old-invoice.pdf
ls: cannot access '/home/alex/downloads/old-invoice.pdf': No such file or directory

The first ls confirms the file exists. rm prints nothing at all on success — silence means it worked. The second ls proves the file is gone.

Example 2: Removing a directory and its contents

rm ~/projects/old-report-drafts

Output:

rm: cannot remove '/home/alex/projects/old-report-drafts': Is a directory

Plain rm refuses directories outright, as a safety measure. Adding -r tells it to recurse:

rm -r ~/projects/old-report-drafts

This succeeds silently, deleting old-report-drafts and every file inside it, no matter how deeply nested.

Example 3: Confirming each deletion with -i

rm -i ~/projects/old-report-drafts/draft1.tmp ~/projects/old-report-drafts/draft2.tmp

Output:

rm: remove regular file '/home/alex/projects/old-report-drafts/draft1.tmp'? y
rm: remove regular file '/home/alex/projects/old-report-drafts/draft2.tmp'? y

-i makes rm ask before touching each file. Typing y and pressing Enter confirms; anything else skips that file and moves to the next.

Example 4: rmdir only removes empty directories

mkdir ~/tmp/empty-cache
rmdir ~/tmp/empty-cache

This succeeds silently — the directory was empty, so rmdir could remove it. Now compare a directory that still has something inside it:

mkdir -p ~/tmp/session-cache/data
rmdir ~/tmp/session-cache

Output:

rmdir: failed to remove '/home/alex/tmp/session-cache': Directory not empty

rmdir refuses because session-cache still contains the data subdirectory. To remove it along with its contents you would need rm -r ~/tmp/session-cache instead.

How it works step by step

Walking through rm -r ~/projects/old-report-drafts:

  • Bash performs tilde expansion, turning ~ into the value of $HOME, producing an absolute path.
  • The shell hands the expanded argument list to the rm binary; rm parses -r as a flag and the path as an operand.
  • rm calls lstat() on the path to check what kind of file it is. Seeing a directory, and having -r, it proceeds instead of erroring out.
  • rm opens the directory and reads its entries one by one. For each subdirectory, it recurses into it first (depth-first traversal); for each regular file, it calls unlink() immediately.
  • Once a directory’s contents are fully removed, rm removes the now-empty directory itself, then returns to its parent and continues.
  • When everything is gone, rm exits with status 0. If any file could not be removed (a permission error, for example), rm reports it, keeps going with the rest, and exits with a non-zero status at the end — check $? immediately after if a script needs to know.

Common Mistakes

Mistake 1: Not quoting a variable that contains spaces

file="old report.txt"
rm $file

Without quotes, Bash word-splits $file on the space before rm ever sees it, turning one filename into two arguments: old and report.txt — neither of which exists, so both fail with No such file or directory, and the real file is never touched.

file="old report.txt"
rm "$file"

Quoting "$file" preserves it as a single argument, exactly as stored in the variable.

Mistake 2: An empty variable turns rm -rf into a disaster

logdir=
rm -rf "$logdir"/*

Quoting alone does not save you here. If logdir was never set — a typo’d variable name, a failed earlier command, a missing config value — it expands to an empty string, and "$logdir"/* becomes /*. Bash’s own globbing then expands that to every top-level entry on the filesystem the user can write to. This exact bug, run as root, has destroyed real production servers.

logdir="/var/log/myapp"
rm -rf -- "${logdir:?}"/*

The ${logdir:?} expansion makes Bash print an error and exit immediately if logdir is unset or empty, instead of silently substituting nothing. The -- also protects against a path that happens to start with a dash being misread as an option.

Other pitfalls to watch for

  • Reaching for rmdir when you actually meant to delete a directory’s contents too — rmdir will just refuse; use rm -r when you intend to remove everything inside.
  • Assuming rm has an undo. It does not. There is no trash by default — recovery after the fact generally requires backups, filesystem snapshots, or specialized (and unreliable) data-recovery tools.
  • Running a wildcard delete like rm *.log from the wrong working directory. Always check pwd, and preview matches with ls *.log before turning that same pattern into an rm command.

Best Practices

  • Preview any wildcard or variable-based deletion with ls or echo first, then swap in rm once you have confirmed the exact list of matches.
  • Use rm -I for bulk or recursive deletions — it prompts once instead of once per file, so it is far less likely to be reflexively confirmed away like repeated -i prompts.
  • In scripts, always quote path variables and guard against empty ones with "${var:?}", and put set -euo pipefail near the top so an unset variable or failed command stops the script instead of continuing into a destructive command.
  • Use -- before paths so a filename that happens to start with a dash is never mistaken for an option.
  • Consider a trash utility such as trash-cli or gio trash for interactive, everyday deletions, reserving raw rm for scripts and situations where you specifically do not want recoverability.
  • Never work as the root user directly for routine cleanup; use sudo rm ... for the one command that needs it so the action is deliberate and logged.
  • For conditional or pattern-based cleanup (for example, files older than 30 days), use find ... -delete rather than piping find into xargs rm, which is easy to get wrong with filenames containing spaces or newlines.

Practice Exercises

  • Create ~/practice/logs containing a couple of files, then try rmdir ~/practice/logs. Observe the error, remove the files individually, and confirm rmdir now succeeds.
  • Write a short script that safely deletes every *.tmp file inside a directory passed in a variable, guarding against that variable being empty with ${var:?} and quoting it properly throughout.
  • Create five empty files in a scratch directory, then compare running rm -i versus rm -I on all five at once. Note how many prompts each produces and why.

Summary

  • rm deletes files (and, with -r, directories and their contents); rmdir only deletes directories that are already empty.
  • Deleting a file calls unlink(), which removes its directory entry and decrements its inode’s link count — data is only reclaimed once that count hits zero and no process still has the file open.
  • There is no trash by default: an rm‘d file is gone unless you have a backup, snapshot, or a trash utility in place.
  • Always quote variables in destructive commands, and guard against empty variables with ${var:?} — quoting alone does not protect against an unset or empty path turning into something dangerous.
  • Prefer rm -I over repeated -i prompts for bulk deletions, and always preview wildcard matches with ls before turning them into an rm command.