ls — list directory contents
Overview
ls lists files and directories, and is simultaneously the most-used Unix command and one of the most option-dense: sorting keys, time sources, quoting styles, colorization, and format controls all live in one binary. It ships in the coreutils package (Debian bookworm: GNU coreutils 9.1) at /usr/bin/ls. Debian interactive shells typically alias it to ls --color=auto, and the color palette itself is generated by sibling dircolors into the LS_COLORS environment variable.
ls is often confused with its GNU siblings dir and vdir (same binary family with different compiled-in defaults), with find (walks trees and filters by predicates rather than listing one level), and with echo */globs (which expand names in the shell without metadata). For scripts, the rule of thumb: ls is for humans; the shell, find, and globbing are for machines.
| Field | Value |
|---|---|
| Package | coreutils (Debian bookworm) |
| Man section | 1 |
| Path | /usr/bin/ls |
| First appeared | AT&T UNIX, Version 1 (1971) |
| Standards | POSIX.1-2018 (ls) |
Synopsis
ls [OPTION]... [FILE]...
Common one-line forms:
ls -l [file...] # long format, one entry per line
ls -lah # long, human sizes, include dotfiles
ls -lt | head # newest files first
ls -d */ # list directories themselves, not contents
How It Works
What gets listed
For each FILE operand that is a file, ls describes that file. For each operand that is a directory, it lists the directory’s contents (sorted, per the active sort key). -d inverts this for directories — describe the directory itself, which is how ls -d */ and ls -ld /var/log work. -a includes entries starting with . (the shell-glob visibility rules have nothing to do with ls; hiding dotfiles is purely an ls convention, with -A dropping only . and ..).
Format defaults: terminal vs pipe
GNU ls changes behavior depending on whether stdout is a terminal:
output to terminal: columns (-C), shell-escape quoting, color if configured
output to a pipe/file: one entry per line (-1), literal quoting, no color
This is intentional POSIX-ish friendliness (piped output is machine-readable) and simultaneously the #1 scripting trap: interactively you see columns; in ls | wc -l you count lines. It is also exactly why dir and vdir exist — they force the column/long formats and backslash-escaped quoting unconditionally, so their output is identical on a terminal or in a pipe.
The long format, field by field
-rw-rw-r-- 1 z z 5 Oct 9 09:33 a3
│ │ │ │ │ │ │ │ └── mtime (style chosen by --time-style)
│ │ │ │ │ │ └──── size in bytes (-h for human units)
│ │ │ │ │ └─────── group (-o hides; -G suppresses in -l)
│ │ │ │ └───────── owner (-g hides; -n prints numeric IDs)
│ │ │ └─────────── hard-link count (nlink)
│ └──┴──┴─────────── mode: type + user/group/other bits (S/Sticky/T specials)
└──────────────────── type char: - file, d dir, l symlink, p FIFO,
b/c device, s socket
Trailing marks in the mode string: . = SELinux context present, + = ACL entries. -F appends type indicators to names (/ dir, * executable, @ symlink, | FIFO, = socket). -i prints the inode number, -s the allocated disk blocks (1 KiB units by default; 512-byte units under POSIXLY_CORRECT — hence the classic “ls -s disagrees with du” question, since du’s default block differs).
Sorting and time sources
Default sort is alphabetical under the current locale’s collation — often case- and punctuation-insensitive in UTF-8 locales, so LC_ALL=C ls is the reproducible ordering for scripts. Sort keys: -t mtime, -c ctime (inode/status change), -u atime, -S size, -X extension, -v natural version sort, -U directory order (fastest, no sorting), -r reverses any of them. With -l, the time column shows mtime unless -c/-u switch both the sort and the displayed timestamp; --full-time prints full ISO timestamps; --time-style=long-iso is the sane default for logs.
Symlinks, recursion, color
Command-line symlinks are dereferenced by default; in listings, -l describes the link (with -> target) unless -L follows it. -R recurses depth-first into subdirectories. Colorization (--color=when) maps entry metadata through LS_COLORS — type codes like di=01;34 plus suffix patterns like *.tar=01;31 — a database generated by dircolors. Colors are cosmetic only; nobody should parse them (see gotchas).
Options That Matter
Output format
| Option | Effect |
|---|---|
-l | Long format, one entry per line with metadata |
-C, -1, -m, -x | Columns / one-per-line / comma-separated / across rows |
-h | Human-readable sizes with -l/-s (1K 234M 2G) |
--si | Powers of 1000 instead of 1024 |
-F, -p | Append type indicators / slash for directories |
-i, -s | Print inode numbers / allocated blocks |
-n, -o, -g, -G | Numeric IDs / omit group / omit owner / omit group |
--time-style=STYLE | long-iso, full-iso, iso, +FORMAT |
--quoting-style=WORD | literal, shell, shell-escape (default on tty), c, escape |
Selection and filtering
| Option | Effect |
|---|---|
-a, -A | Include dot entries / same minus . and .. |
-d | List directories themselves, not their contents |
-I PATTERN | Ignore matching entries (e.g. -I '*.pyc') |
-B | Ignore ~ backup files |
-R | Recurse into subdirectories |
--group-directories-first | Dirs before files (GNU) |
Sorting
| Option | Effect |
|---|---|
-t, -c, -u | Sort by mtime / ctime / atime (newest first) |
-S, -X, -v | Size / extension / version (natural) order |
-U | No sorting — directory order (fastest) |
-r | Reverse the current sort |
--sort=WORD | Explicit key: time, size, extension, version, none |
Usage Patterns
# The everyday: long format, human sizes, all entries
ls -lah
# Newest first — "what changed in here?"
ls -lt | head
# Largest files in a tree level, largest last
ls -lSh | head
# Natural version sort that survives v2 vs v10 comparisons
ls -v releases/
# Describe directories themselves (glob stays unexpanded-safe)
ls -ld /etc/* | head
# Only show directories in this level
ls -d */
# Script-stable long listing: numeric IDs, ISO timestamps, no locale drift
LC_ALL=C ls -ln --time-style=long-iso
# Full ISO time for audit logs
ls -l --full-time /var/log/audit/
# Change tracking by status-change time (perms, ownership, hard links)
ls -lct /etc | head
# Where did the disk go? allocated blocks, not apparent size
ls -lsSh /var/lib/docker | head
# Pair with find when you need recursion and predicates
find . -maxdepth 1 -name '*.conf' -exec ls -l {} +
# Classify output at a glance: dirs with /, executables with *, links with @
ls -F /usr/bin | head
# Ignore noise in node_modules-style trees
ls -I node_modules -I '*.log' -lah
# Inode hunting before an NFS/debugfs session
ls -i | head
Nuances and Gotchas
- Never parse
lsoutput in scripts. Filenames may contain spaces, newlines, and quotes; column formats break across locales;ls | wc -lmiscounts when output wraps or quoting changes. Use globs,find -print0 | xargs -0, orfor f in *— the shell owns the filename list. - Piped vs terminal defaults.
lsin a pipe prints one name per line literally — no columns, no shell-escape quoting, no color. Scripts relying on what they saw interactively (or copying column-basedawkfield picks) break the moment output is redirected. - Quoting is version-dependent. GNU 8.25+ defaults to
shell-escapequoting on terminals, so a filename with a tab prints asweird-name-\tx.txt; older releases printed control characters raw.QUOTING_STYLEand-N(literal) change it. - Locale collation.
[A-Z]-style expectations fail in UTF-8 locales where ordering ignores case/punctuation; prefix time- or log-based commands withLC_ALL=Cwhen order matters. - mtime is not ctime.
-tsorts by content modification;-cby inode/status change (chmod, chown, link count, rename). Logs that “didn’t change” but changed owner show up under-ct, not-lt. -svs apparent size.ls -lshows bytes the file claims;ls -s/dushow blocks actually allocated — sparse files and tail-block rounding make them disagree, and the disagreement units differ between GNU (KiB), POSIX (512 B) anddudefaults.- Color in scripts.
--color=auto(the usual alias) emits escapes only on a tty; forcing--color=alwaysinto pipes leaks ANSI codes into logs and greps. Strip withsed 's/\x1b\[[0-9;]*m//g'when you inherit polluted output. - Dangling symlinks still list.
ls -lshows them (thanks tolstat);ls -Lon them errors — which is why “ls shows it but cat can’t read it” puzzles people. - Portability. GNU-only:
-v,--time-style,--color,--hyperlink,--group-directories-first,-I. BSD/macOS has most core flags but different long-format details; BusyBoxlsis a subset. POSIX pins-l,-t,-r,-a,-A,-R,-C,-1,-f,-i,-s,-F,-m,-x,-c,-u.
Exit Status
| Code | Meaning |
|---|---|
| 0 | Listed everything requested |
| 1 | Minor problems (e.g. an unreadable subdirectory encountered during -R) |
| 2 | Serious trouble (e.g. cannot access a command-line argument) |
Related Commands
dir— the always-columnar GNU sibling (ls -C -bsemantics).vdir— the always-long-format GNU sibling (ls -l -bsemantics).dircolors— generates theLS_COLORSdatabase--colorconsumes.ln— why the nlink column and->arrows look the way they do.cp— the copying counterpart for the files you just listed.- Collection overview — the GNU Coreutils chapter hub.
- find — recursive, predicate-based listing when one level isn’t enough.
- xargs — the correct way to feed filenames to commands (instead of parsing
ls). - man pages — where the full option matrix of
lsis documented.
Interview Questions
Q: Why is “parse the output of ls” considered a bug, and what do you use instead?
ls output is formatted for humans: columns wrap at terminal width, quoting styles vary (shell-escape, literal, c), locale collation reorders names, and filenames can contain newlines that make line-based parsing ambiguous or outright malicious (a file named x\nrm -rf ~). The machine-readable alternatives are the shell’s own globbing, find -print0 | xargs -0 for arbitrary trees, and ls -1 only as a last resort on names you already control.
Q: What is the difference between mtime, ctime, and atime, and which ls flags expose them?
mtime = file content last modified; ctime = inode status last changed (mode, ownership, link count, rename, size change); atime = last read. ls -l shows mtime by default; -c switches display and sort to ctime, -u to atime. Interview-worthy subtlety: renaming a file changes ctime but not mtime, and changing permissions changes ctime without touching mtime — which is why timestamp-only tamper checks fail.
Q: Explain what ls -l’s link-count column means for files and for directories.
For files it’s the hard-link count: how many directory entries name the inode. A value above 1 means the data is reachable under multiple names (see ln, cp -l). For directories, POSIX-style filesystems give each directory two inherent links (. and its parent’s entry) plus one per subdirectory’s .., so the count is subdirectories + 2 — a quick way to count immediate subdirectories without spawning a process.
Q: What changes about ls output when you pipe it, and why does dir exist?
When stdout is not a terminal, GNU ls switches to one-entry-per-line, literal quoting, and no color — machine-friendly defaults. dir is compiled from the same source with -C -b (columns plus backslash-escaped quoting) forced on regardless of the output type, and vdir does the same for -l -b. They exist for MS-DOS-era familiarity and for stable output; for scripts the safer lesson is to pass explicit flags rather than rely on defaults.
Q: How does ls --color decide what color each entry gets, and where does the palette come from?
ls reads the LS_COLORS environment variable: a colon-separated list where keys are file-type codes (di, ln, ex, fi, …) or glob suffixes (*.tar=01;31), values are SGR sequences. The palette is generated by dircolors (matching on $TERM) and imported into the shell environment via eval "$(dircolors -b)". Missing keys fall back to compiled-in defaults; color codes are presentation only and must never be parsed.
Q: A 100 GB sparse disk image shows 5 GB with ls -s but 100 GB with ls -l. Explain both numbers.
ls -l reports the apparent size: how many bytes the file logically contains (the full 100 GB address space). ls -s reports allocated blocks: disk actually reserved (5 GB) because holes were never written. Copying matters here: plain cp keeps sparseness (--sparse=auto), --sparse=never inflates the copy to real blocks, and du --apparent-size vs du shows the same duality from the usage side.