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
rmbinary;rmparses-ras a flag and the path as an operand. rmcallslstat()on the path to check what kind of file it is. Seeing a directory, and having-r, it proceeds instead of erroring out.rmopens 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 callsunlink()immediately.- Once a directory’s contents are fully removed,
rmremoves the now-empty directory itself, then returns to its parent and continues. - When everything is gone,
rmexits with status0. If any file could not be removed (a permission error, for example),rmreports 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
rmdirwhen you actually meant to delete a directory’s contents too —rmdirwill just refuse; userm -rwhen you intend to remove everything inside. - Assuming
rmhas 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 *.logfrom the wrong working directory. Always checkpwd, and preview matches withls *.logbefore turning that same pattern into anrmcommand.
Best Practices
- Preview any wildcard or variable-based deletion with
lsorechofirst, then swap inrmonce you have confirmed the exact list of matches. - Use
rm -Ifor 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-iprompts. - In scripts, always quote path variables and guard against empty ones with
"${var:?}", and putset -euo pipefailnear 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-cliorgio trashfor interactive, everyday deletions, reserving rawrmfor scripts and situations where you specifically do not want recoverability. - Never work as the
rootuser directly for routine cleanup; usesudo 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 ... -deleterather than pipingfindintoxargs rm, which is easy to get wrong with filenames containing spaces or newlines.
Practice Exercises
- Create
~/practice/logscontaining a couple of files, then tryrmdir ~/practice/logs. Observe the error, remove the files individually, and confirmrmdirnow succeeds. - Write a short script that safely deletes every
*.tmpfile 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 -iversusrm -Ion all five at once. Note how many prompts each produces and why.
Summary
rmdeletes files (and, with-r, directories and their contents);rmdironly 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 -Iover repeated-iprompts for bulk deletions, and always preview wildcard matches withlsbefore turning them into anrmcommand.
