I type ls more than any other command in my entire terminal history, probably by a wide margin. It seems trivial — list what’s in a directory — but the flag combinations I reach for (ls -la, ls -lh, ls -ltr) actually encode a lot of Unix filesystem knowledge once you look closely: inode numbers, sparse file sizes vs. block usage, sort orders, and how coloring and formatting get decided. This guide covers ls from the basics through to the internals and the workflows I actually use.
What Is the ls Command?
ls lists directory contents. It’s part of GNU coreutils on virtually every Linux distribution:
$ ls --version
ls (GNU coreutils) 9.4
Copyright (C) 2023 Free Software Foundation, Inc.
Basic Syntax
ls [OPTION]... [FILE]...
With no arguments, ls lists the contents of the current directory. With one or more paths, it lists each; for a file argument (not a directory), it lists just that file’s own entry.
How ls Works Internally
When you run ls, it calls opendir()/readdir() on the target directory, which reads the raw list of (name, inode number) entries stored in the directory’s own data — the same structure I described in the mkdir article. For a bare ls, that’s actually all it needs: just the names.
The moment you add -l (long format), ls has to call stat() (or lstat() for symlinks, so it doesn’t follow them) on every single entry to retrieve permissions, owner, group, size, and modification time. This is why ls -l on a directory with tens of thousands of files is noticeably slower than a bare ls — it’s doing one extra system call per file rather than a single directory read.
$ ls -la
total 12
drwxr-xr-x 3 root root 4096 Jul 31 01:36 .
drwxr-xr-x 3 root root 4096 Jul 31 01:36 ..
drwxr-xr-x 3 root root 4096 Jul 31 01:36 a
-rw-r--r-- 1 root root 0 Jul 31 01:36 file2.txt
Adding -i shows the inode number stat() retrieved, letting you directly see when two names share one file (hard links):
$ ls -li
573462 -rwxr-xr-x 2 root root 6 Jul 31 01:36 hardlink.txt
573462 -rwxr-xr-x 2 root root 6 Jul 31 01:36 src.txt
Both hardlink.txt and src.txt show inode 573462 — proof they’re the same underlying file with two names, exactly as discussed under ln.
Full List of Common Parameters
ls has dozens of flags; here are the ones actually worth knowing, grouped by purpose.
Display format:
| Option | Long form | Description |
|---|---|---|
-l | Long listing format (permissions, owner, group, size, date) | |
-1 | One entry per line | |
-C | Multi-column output (default when writing to a terminal) | |
-m | Comma-separated list | |
-x | List entries by lines instead of columns |
Content and filtering:
| Option | Long form | Description |
|---|---|---|
-a | --all | Show hidden files (those starting with .) |
-A | --almost-all | Like -a but excludes . and .. |
-d | --directory | List directories themselves, not their contents |
-R | --recursive | List subdirectories recursively |
Sorting:
| Option | Long form | Description |
|---|---|---|
-t | Sort by modification time, newest first | |
-S | Sort by file size, largest first | |
-X | Sort by extension | |
-r | --reverse | Reverse the sort order |
-U | No sorting; list in directory order (fastest) | |
--sort=WORD | Explicit sort key: none, size, time, version, extension |
Size and time display:
| Option | Long form | Description |
|---|---|---|
-h | --human-readable | Sizes in human-readable form (K, M, G) with -l |
-s | --size | Print allocated block size for each file |
--time=WORD | Which timestamp to show: atime, ctime, mtime (default) | |
--full-time | Show full ISO-format timestamps |
Other useful flags:
| Option | Long form | Description |
|---|---|---|
-i | --inode | Print the inode number of each file |
-F | --classify | Append indicator (/, *, @, etc.) revealing file type |
-G | --no-group | Omit the group column in long listing |
--color=WHEN | Colorize output: always, auto, never | |
-p | Append / to directory names |
Practical Examples with Output
Long listing with human-readable sizes:
$ ls -lh
total 4.0K
drwxr-xr-x 3 root root 4.0K Jul 31 01:36 a
-rw-r--r-- 1 root root 0 Jul 31 01:36 file2.txt
Recursive listing:
$ ls -R a
a:
b
a/b:
c
a/b/c:
Sort by modification time, newest first (great for finding recently changed files):
$ ls -lt
Sort by size, largest first, human-readable:
$ ls -lhS
Classify entries to distinguish files, directories, and links at a glance:
$ ls -F
a/ file2.txt
The trailing / marks a as a directory; executables get a trailing *, symlinks get @.
Showing hidden files:
$ ls -A
.bashrc a file2.txt
Common Use Cases
- Quickly checking what’s in the current directory
- Auditing file permissions and ownership before a chmod/chown operation (
ls -l) - Finding the largest files in a directory (
ls -lhS) - Finding recently modified files (
ls -lt) - Spotting hard links by shared inode number (
ls -li) - Verifying hidden config files exist (
ls -a)
Shell Scripting and Automation
A word of caution I want to be upfront about: parsing ls output in scripts is widely considered bad practice, because filenames can contain spaces, newlines, and other characters that break naive parsing. For scripts, prefer globbing or find with -print0:
# Fragile — avoid
for f in $(ls *.log); do
echo "$f"
done
# Robust — prefer this
for f in *.log; do
[[ -e "$f" ]] || continue
echo "$f"
done
ls is genuinely great for interactive, human-facing use, but for anything a script needs to act on programmatically, find, globs, or stat are the correct tools.
Where ls is fine in scripts is for pure informational/logging output, not as a data source to loop over:
echo "Current directory contents:"
ls -lh /var/log/myapp
Real-World System Administration Workflows
- Disk usage triage:
ls -lhS /var/log | headto quickly spot the largest log files during a “disk full” incident. - Change auditing:
ls -lt /etc | headafter a system change to see what configuration files were touched most recently. - Deployment verification:
ls -la /opt/myapp/currentto confirm a deployment symlink and its target files are present and correctly owned. - Security review:
ls -la ~/.sshto eyeball permission bits on key files, since SSH refuses overly-permissive private keys.
Comparing ls to Related Commands
lsvsfind:findis built for programmatic, filterable, recursive search and is the right choice inside scripts;lsis built for fast, human-readable, interactive browsing.lsvstree:tree(a separate package on most distros) draws a visual hierarchy of nested directories, whichls -Rtechnically also lists but in a much less readable flat format.lsvsstat:statgives complete metadata for one file at a time (all timestamps, block size, inode, etc.) in more detail thanls -lshows per column; uselsfor browsing many files at once,statfor deep-diving one.lsvsexa/eza: modern Rust-based replacements add git status integration and nicer default coloring/icons, but aren’t installed by default on most systems the way GNUlsis.
Troubleshooting Common ls Issues
Colors look wrong or missing when piping output: ls disables color by default when output isn’t a terminal (e.g., piped to less or redirected to a file) unless you explicitly pass --color=always.
ls -l shows a ? for permissions or size: this usually means the file was deleted or became inaccessible between the directory read and the stat() call — a race condition, not a bug, common on very active directories.
Sort order looks unexpected with -t: check --time=WORD; by default -t sorts by modification time (mtime), not access time (atime) or change time (ctime) — pick the right one for what you’re actually investigating.
Filenames with spaces or special characters break a script using ls output: switch to globbing or find -print0/xargs -0 as shown above.
Performance Considerations
A bare ls on a huge directory is fast because it’s a single directory read. ls -l on the same directory is much slower because of the per-file stat() calls — on network filesystems (NFS especially) this difference can be dramatic, sometimes minutes versus seconds on directories with tens of thousands of entries. When you don’t need per-file metadata, skip -l. If you only need to know whether a directory is non-empty, ls -U (unsorted) skips the extra sort pass as well.
Security Implications
ls -l output is often the first thing I check when auditing permissions, and it’s worth remembering it reflects only standard Unix permission bits — it doesn’t show ACLs (access control lists) or extended attributes by default. A file can look like -rw-r--r-- in plain ls -l while an ACL grants extra access to a specific user or group; the + suffix on the permission string (-rw-r--r--+) is the tell that ACLs are present, and you’d need getfacl to see the actual entries. Don’t rely on ls -l alone as a complete security audit for permission-sensitive directories.
Best Practices
- Use
ls -lhfor human-friendly size review,ls -lSwhen hunting for space hogs. - Use
ls -lito spot hard links by matching inode numbers. - Never parse
lsoutput in scripts — use globs orfindinstead. - Check for a trailing
+inls -loutput as a sign ACLs need a closer look withgetfacl. - Use
--color=auto(usually the distro default) so color doesn’t leak into piped/redirected output.
Compatibility Across Distributions
GNU ls is consistent across Debian/Ubuntu, RHEL/Fedora, Arch, and openSUSE, though many distros ship a default alias ls='ls --color=auto' in .bashrc, which is a shell alias, not a difference in the binary itself. BusyBox ls (Alpine, minimal containers) supports the core flags (-l, -a, -h, -R) but has a much smaller set of sort and formatting options — check ls --help in minimal images before relying on --sort=WORD or --time=WORD.
Advanced Scenarios I’ve Run Into
Distinguishing directories, executables, and symlinks in a script-safe way — -F appends type indicators (/ for directories, * for executables, @ for symlinks, | for FIFOs, = for sockets) that are handy for quick visual scanning, but for anything a script actually needs to branch on, test/[[ ]] with -d, -x, -L is the correct, unambiguous approach rather than parsing -F output.
Comparing directory contents at a glance across two locations:
$ diff <(ls dir1) <(ls dir2)
This uses process substitution to feed each ls output into diff as if it were a file, giving a quick side-by-side of what differs between two directory listings — genuinely useful for a fast sanity check, though for anything beyond filenames (content differences, permission differences) diff -r or rsync -avn --delete are more thorough tools.
Showing only directories, not files, in a listing:
$ ls -d */
The trailing / in the glob pattern itself (not an ls flag) restricts the match to directories, and -d prevents ls from then listing each directory’s contents — without -d, ls would recurse one level into every matched directory instead of just naming it.
Getting a full ISO-format timestamp instead of the abbreviated default, useful when scripting around exact modification times or when default relative/abbreviated formatting is ambiguous across years:
$ ls -l --full-time file2.txt
-rw-r--r-- 1 root root 0 2026-07-31 01:36:14.822384511 +0000 file2.txt
Why ls Output Format Changes Depending on Terminal vs Pipe
ls actively checks whether its standard output is connected to a terminal (via isatty()) and changes behavior accordingly: multi-column layout and color are enabled by default only when writing to a terminal, while output redirected to a file or piped to another command automatically switches to one-entry-per-line, uncolored output — the sensible default for anything meant to be machine-read rather than eyeballed. This is why ls in a terminal looks different from ls | cat, even with identical flags, and it’s a deliberate design choice rather than an inconsistency.
Summary
ls earns its place as the most-used Linux command because it’s the fastest way to answer “what’s here, and what does it look like.” Once you know that a bare listing is a single directory read while -l triggers a stat() per entry, the performance characteristics and the right flag choices for a given job both become obvious. For humans, ls -lh, ls -lt, and ls -li cover the vast majority of real needs; for scripts, it’s almost always better to reach for globbing or find instead.
References
- GNU Coreutils Manual —
lsinvocation: https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html - Linux man-pages project —
ls(1): https://man7.org/linux/man-pages/man1/ls.1.html - Why parsing
lsis problematic (Greg’s Wiki / BashFAQ): https://mywiki.wooledge.org/ParsingLs - Ubuntu Manpage Repository: https://manpages.ubuntu.com/manpages/noble/en/man1/ls.1.html
