Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

unlink — remove a file by calling the unlink system call

Overview

unlink removes exactly one file by calling the unlink(2) system call — nothing more. No options, no recursion, no prompting, no globbing of its own, no -f to silence errors. It is the thinnest possible command-line wrapper around the kernel operation that deletes a directory entry and decrements the file’s link count.

It ships in the Debian coreutils package at /usr/bin/unlink and is POSIX-standardized. Its niche is precision: scripts that must delete a single known path and fail loudly if that fails — atomic-replace bookkeeping, stale lockfile cleanup, trap handlers — reach for unlink because rm’s convenience features (recursion, -f, alias-driven prompts) are exactly the behaviours a careful script does not want.

It is often confused with rm (the general-purpose remover) and with rename-based atomic replacement (which unlink alone cannot provide).

FieldValue
Packagecoreutils (Debian bookworm)
Section (man)1
Path/usr/bin/unlink
First appeared / lineageUnix v1-era rm/unlink split; in GNU coreutils
StandardsPOSIX.1-2017

Synopsis

unlink FILE
unlink /tmp/lockfile        # remove one path, period
unlink /tmp/stale-link      # removes the symlink, never its target

How It Works

A Unix file’s data survives until its link count reaches zero and no process holds it open. unlink(2) performs the first half: it removes one directory entry (name) and decrements the inode’s link count. When the count hits zero and the last open descriptor closes, the kernel frees the inode and data blocks. unlink the command simply marshals one path argument into that syscall.

   before                        unlink("/tmp/data")         after
┌──────────────┐            ┌─────────────────────┐   ┌──────────────┐
│ dir /tmp     │            │ remove dir entry    │   │ dir /tmp     │
│  data → 4210 │  ────────▶ │ inode 4210 count    │──▶│  (no 'data') │
│ inode 4210: 3 links       │ 3 → 2               │   │ inode 4210: 2 links
│ (hard links elsewhere survive)                        │
└──────────────┘            └─────────────────────┘   └──────────────┘

Consequences that follow directly from the syscall semantics:

$ echo x > /tmp/target; ln -s /tmp/target /tmp/lnk
$ unlink /tmp/lnk          # the symlink itself, not /tmp/target
$ cat /tmp/target          # target untouched
x

Directories are refused: unlink(2) on a directory fails (EISDIR on Linux); the dedicated syscall for directories is rmdir(2), which rm uses internally. Deleting a name requires write permission on the containing directory (plus, on sticky directories like /tmp, ownership of the file or the directory) — not write permission on the file itself, a permission model surprise that predates this utility.

The practical payoff of such a minimal tool is failure honesty. There is no -f, so every failure surfaces:

$ unlink /tmp/no-such-file
unlink: cannot unlink '/tmp/no-such-file': No such file or directory
$ echo $?
1

Options That Matter

OptionEffect
(none)Only --help and --version exist; there are no operational flags

Exactly one operand is accepted; a second is a usage error. If a shell glob expands to several names, unlink rejects the invocation rather than guessing — the shell expands globs before unlink runs, so unlink *.log with multiple matches is an error, which is usually the safety property the script author wanted.

Usage Patterns

# Precise single-file removal in a script — no aliases, no prompts, no -f
unlink "$WORKDIR/result.tmp"
# Stale lockfile cleanup on startup
[ -f /var/run/myjob.lock ] && unlink /var/run/myjob.lock
# trap-based temp cleanup that can never over-delete
TMP=$(mktemp /tmp/job.XXXXXX)
trap 'unlink "$TMP"' EXIT
# Atomic-ish publish: write to temp, then swap names with mv (rename(2))
( generate > "$DIR/.draft" ) && mv -f "$DIR/.draft" "$DIR/published"
# Rotating a single file without rm's option parsing
unlink /var/log/app.log.9
# Removing a dangling symlink safely (never follows the link)
unlink /var/run/service.pid   # if it happens to be a symlink, still fine
# Fail-loud deletion where rm -f would hide a path bug
unlink "$DIR/$FILE" || { echo "cleanup failed for $DIR/$FILE" >&2; exit 1; }
# Deleting one entry from a generated list, one call per name (xargs -n1)
find . -name '*.partial' -print0 | xargs -0 -n1 unlink

Nuances and Gotchas

  • Globs are a shell thing. unlink *.tmp passes every match as separate operands and fails on the first extra one. This surprises people who expect rm-like glob handling; it is also the guardrail that makes unlink safe in scripts.
  • Deleted-but-open files keep consuming space. If a process holds the file open (a log being written), unlinking succeeds but the disk is freed only when the descriptor closes — the classic “df says full, du says empty” mystery with fat log files.
  • No -f, no -r, no prompts. Removal of a read-only file succeeds if the directory is writable — permission checks target the directory, not the file. Conversely, unwritable directory means failure with a clear errno message.
  • Directories always fail with EISDIR; use rmdir (or rm -d) for empty directories. Nothing in unlink will recurse, ever.
  • Atomic replacement is rename’s job. unlink + recreate is a two-step window where readers see no file; the atomic pattern is write-to-temp + mv (which calls rename(2)). Unlink belongs to the cleanup half of such schemes.
  • NFS and sticky-bit oddities: deleting an open file on NFS can leave .nfsXXXX zombies, and on sticky directories (/tmp) you can only unlink files you own — both are kernel behaviours, not unlink options.

Exit Status

StatusMeaning
0The file was unlinked successfully
1Failure: nonexistent path, permissions, directory operand, wrong argument count

Diagnostics come verbatim from errno via coreutils’ standard error formatting, which is why the failure message is as precise as the syscall.

  • true — the exit-status contract style (0 on success, nonzero diagnostics) unlink follows.
  • timeout — bounds cleanup loops that repeatedly unlink lockfiles.
  • coreutils collection — sibling GNU coreutils pages.

Interview Questions

rm is the user-facing remover: globs, -r for trees, -f to suppress errors, -i prompting, and internal delegation to rmdir for directories. unlink is one syscall with one operand and no flags. The difference matters in scripts: unlink fails loudly on any surprise (extra operand, missing file), which converts silent cleanup bugs into visible errors; rm -f in a script can hide path bugs that delete the wrong file or fail to delete at all.

The directory entry disappears immediately — new opens fail with ENOENT — but the inode’s link count simply drops; data blocks are freed only when the link count reaches zero and the last open file descriptor closes. Processes with the file open continue reading and writing a file that no longer has a name. This is the mechanism behind both the “deleted the log but disk stayed full” mystery and the create-temp-then-unlink-while-open pattern used for scratch files.

Q: Why does deleting a file sometimes require no permission on the file itself?

Unlinking modifies the directory (removing an entry), so the kernel checks write (and search) permission on the containing directory, not the file. On directories with the sticky bit set (/tmp), an additional rule applies: only the file’s owner, the directory’s owner, or root may unlink a given entry. File mode bits like -w are irrelevant to whether the name can be removed.

Write the new content to a temporary file in the same directory, fsync it, then mv it over the target — rename(2) replaces the directory entry atomically, so readers see either the old or the new file, never a partial one. unlink is not part of the atomic step itself; it handles the surrounding bookkeeping: removing stale temporaries, old rotated generations, and failed drafts. Using unlink + recreate for the swap instead of rename would expose a window with no file at all.

References