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

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.

FieldValue
Packagecoreutils (Debian bookworm)
Man section1
Path/usr/bin/ls
First appearedAT&T UNIX, Version 1 (1971)
StandardsPOSIX.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.

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

OptionEffect
-lLong format, one entry per line with metadata
-C, -1, -m, -xColumns / one-per-line / comma-separated / across rows
-hHuman-readable sizes with -l/-s (1K 234M 2G)
--siPowers of 1000 instead of 1024
-F, -pAppend type indicators / slash for directories
-i, -sPrint inode numbers / allocated blocks
-n, -o, -g, -GNumeric IDs / omit group / omit owner / omit group
--time-style=STYLElong-iso, full-iso, iso, +FORMAT
--quoting-style=WORDliteral, shell, shell-escape (default on tty), c, escape

Selection and filtering

OptionEffect
-a, -AInclude dot entries / same minus . and ..
-dList directories themselves, not their contents
-I PATTERNIgnore matching entries (e.g. -I '*.pyc')
-BIgnore ~ backup files
-RRecurse into subdirectories
--group-directories-firstDirs before files (GNU)

Sorting

OptionEffect
-t, -c, -uSort by mtime / ctime / atime (newest first)
-S, -X, -vSize / extension / version (natural) order
-UNo sorting — directory order (fastest)
-rReverse the current sort
--sort=WORDExplicit 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 ls output in scripts. Filenames may contain spaces, newlines, and quotes; column formats break across locales; ls | wc -l miscounts when output wraps or quoting changes. Use globs, find -print0 | xargs -0, or for f in * — the shell owns the filename list.
  • Piped vs terminal defaults. ls in 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-based awk field picks) break the moment output is redirected.
  • Quoting is version-dependent. GNU 8.25+ defaults to shell-escape quoting on terminals, so a filename with a tab prints as weird-name-\tx.txt; older releases printed control characters raw. QUOTING_STYLE and -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 with LC_ALL=C when order matters.
  • mtime is not ctime. -t sorts by content modification; -c by inode/status change (chmod, chown, link count, rename). Logs that “didn’t change” but changed owner show up under -ct, not -lt.
  • -s vs apparent size. ls -l shows bytes the file claims; ls -s/du show blocks actually allocated — sparse files and tail-block rounding make them disagree, and the disagreement units differ between GNU (KiB), POSIX (512 B) and du defaults.
  • Color in scripts. --color=auto (the usual alias) emits escapes only on a tty; forcing --color=always into pipes leaks ANSI codes into logs and greps. Strip with sed 's/\x1b\[[0-9;]*m//g' when you inherit polluted output.
  • Dangling symlinks still list. ls -l shows them (thanks to lstat); ls -L on 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; BusyBox ls is a subset. POSIX pins -l, -t, -r, -a, -A, -R, -C, -1, -f, -i, -s, -F, -m, -x, -c, -u.

Exit Status

CodeMeaning
0Listed everything requested
1Minor problems (e.g. an unreadable subdirectory encountered during -R)
2Serious trouble (e.g. cannot access a command-line argument)
  • dir — the always-columnar GNU sibling (ls -C -b semantics).
  • vdir — the always-long-format GNU sibling (ls -l -b semantics).
  • dircolors — generates the LS_COLORS database --color consumes.
  • 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 ls is 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.

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.

References