Why this matters
Plain ls shows names. When something goes wrong (a script cannot read a file, a disk is full, a "text" file is really binary, a config change did not take effect) you need the rest of the story: who owns the file, who may read it, how big it is, when it changed, and what kind of thing it is. This lesson teaches you to read that information at a glance, and introduces the inode, the kernel's real record of a file.
Hidden files
Open the shell for this lesson. Any name starting with a dot is hidden: ls skips it unless you ask.
$ ls survey
archive notes.txt report.pdf todo.md
$ ls -a survey
. .. .cache.db .hidden-note .profile archive notes.txt report.pdf todo.md
$ ls -A survey
.cache.db .hidden-note .profile archive notes.txt report.pdf todo.md
There is nothing special about hidden files to the kernel; it is purely a convention of ls (and of shell wildcards, as you will see). It keeps configuration files like ~/.bashrc and ~/.profile out of the way. Run ls -A ~ to see the ones in your home directory.
The long listing
ls -l shows one line of detail per entry:
$ ls -l survey
total 20
drwxr-xr-x. 1 learner learner 14 Nov 1 14:20 archive
-rw-r--r--. 1 learner learner 1200 Nov 1 14:20 notes.txt
-rw-r--r--. 1 learner learner 5000 Nov 1 14:20 report.pdf
-rw-r--r--. 1 learner learner 300 Nov 1 14:20 todo.md
Taking one line apart:
-rw-r--r--. 1 learner learner 1200 Nov 1 14:20 notes.txt
│└───┬───┘│ │ └──┬──┘ └──┬──┘ └┬─┘ └────┬─────┘ └───┬───┘
│ │ │ │ │ │ │ │ name
│ │ │ │ │ │ │ last modification time
│ │ │ │ │ │ size in bytes
│ │ │ │ │ group
│ │ │ │ owner (user)
│ │ │ link count (names pointing at this file; module 3)
│ │ a dot or + here marks extra security info (SELinux label, ACL)
│ permissions: owner rwx, group rwx, others rwx (module 4)
type
total 20 at the top is the disk space used by the listed files, in 1024-byte blocks.
Useful companions:
| Option | Effect |
|---|---|
-h | human-readable sizes (4.9K, 12M) together with -l |
-t | sort by modification time, newest first |
-S | sort by size, largest first |
-r | reverse the sort order |
-d | list a directory itself rather than its contents |
-R | recurse into subdirectories |
-i | show inode numbers |
-F | append a marker: / directory, * executable, @ symlink |
They combine: ls -lhAS is "long, human sizes, almost all, by size", and ls -ltr ("newest last") is a classic for finding what changed recently in a log directory.
File types
The first character of the long listing is the type:
| Char | Type | Example |
|---|---|---|
- | regular file: any bytes (text, image, program) | /etc/hostname |
d | directory | /etc |
l | symbolic link: a pointer to another path | /bin |
c | character device: a byte stream device | /dev/null, /dev/tty |
b | block device: a disk | /dev/sda (not in containers) |
p | named pipe (FIFO) | made with mkfifo |
s | socket: a communication endpoint | /run/... on full systems |
$ ls -ld /etc /bin /dev/null
lrwxrwxrwx. 1 root root 7 Jan 1 2025 /bin -> usr/bin
crw-rw-rw-. 1 root root 1, 3 Nov 1 14:00 /dev/null
drwxr-xr-x. 1 root root 1262 Nov 1 09:01 /etc
For a device, the size column is replaced by two numbers (major, minor) that identify the driver in the kernel.
Notice what the type does not include: whether something is a text file, a JPEG or a program. To the kernel those are all regular files, just bytes. Extensions like .txt and .jpg are a naming convention for humans and some applications; the kernel ignores them.
The file command: what is in it?
file reads the first bytes of a file and compares them with a database of known signatures (magic numbers), so it tells you what the content actually is, whatever the name says:
$ file /etc/hostname /usr/bin/ls /bin /dev/null
/etc/hostname: ASCII text
/usr/bin/ls: ELF 64-bit LSB pie executable, x86-64, ...
/bin: symbolic link to usr/bin
/dev/null: character special (1/3)
A gzip file always starts with the bytes 1f 8b, a PNG with 89 50 4E 47 (.PNG), a script with #!. Use file before you cat something unknown; dumping a binary file to the terminal can scramble it (if it happens, type reset and press Enter).
stat: everything the kernel knows
stat prints the full record:
$ stat survey/notes.txt
File: survey/notes.txt
Size: 1200 Blocks: 8 IO Block: 4096 regular file
Device: 0,52 Inode: 16431 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ learner) Gid: ( 1000/ learner)
Access: 2026-11-01 14:20:03.000000000 +0000
Modify: 2026-11-01 14:20:03.000000000 +0000
Change: 2026-11-01 14:20:03.000000000 +0000
Birth: 2026-11-01 14:20:03.000000000 +0000
What happens underneath: the inode
A file on a Unix filesystem has two parts:
directory "survey" inode 16431 data blocks
┌───────────────┬───────┐ ┌──────────────────────────┐ ┌──────────┐
│ notes.txt │ 16431 ├─────▶│ type: regular file │────▶│ 1200 │
│ report.pdf │ 16432 │ │ size: 1200, blocks: 8 │ │ bytes of │
│ ... │ │ │ owner 1000, group 1000 │ │ content │
└───────────────┴───────┘ │ mode 0644, links: 1 │ └──────────┘
│ atime, mtime, ctime │
└──────────────────────────┘
- The directory is just a table mapping names to inode numbers.
- The inode (index node) holds everything about the file except its name: type, permissions, owner, size, timestamps, link count, and where the data is on disk.
- The data blocks hold the content.
ls -l and stat mostly show you the inode. The name lives only in the directory, which is why renaming a file does not change its modification time, and why one file can have two names (module 3).
The three classic timestamps:
- mtime (Modify): the content last changed. This is what
ls -lshows. - ctime (Change): the inode last changed (content, permissions, owner, link count). Not "creation time", despite the name.
- atime (Access): last read. Many systems update it lazily, so do not rely on it.
- Birth, where the filesystem supports it, is the real creation time.
stat -c prints chosen fields, handy in scripts: stat -c '%s %n' survey/* prints size and name; %i is the inode, %U the owner, %a the permissions in octal.
How much space?
ls -l shows the size of each file, but not the total size of a directory's contents. That is du (disk usage):
$ du -sh survey
32K survey
$ du -h survey
8.0K survey/archive
32K survey
-s gives just the total, -h human units. df -h (previous lesson) shows the free space of whole filesystems.
Common mistakes
- Reading a directory's size as its contents' size. A directory's own size is the space its name table takes. Use
du -sh dir. - Trusting extensions.
photo.jpgcan be text;filetells the truth. - Missing hidden files.
lsalone never shows them; a directory that "looks empty" may not be.rmdirwill then fail withDirectory not empty. - Calling ctime "creation time". It is the inode-change time.
- Parsing
ls -loutput in scripts. Its format varies (dates switch to show the year for old files, names can contain spaces or even newlines). Usestat -cor the tools you will learn later (find -printf).
Recap
- Names starting with
.are hidden fromls(use-aor-A). ls -lshows type, permissions, link count, owner, group, size, mtime and name;-h -t -S -r -dadjust it.- The type is about what kind of object it is (file, directory, link, device), not its content format;
fileinspects content. - A directory maps names to inode numbers; the inode holds the metadata; data blocks hold the content.
statshows the inode. dumeasures space used by contents,dffree space per filesystem.
Going further: run stat /dev/null /proc/uptime and compare them with a normal file. Then ls -li /usr/bin | sort -n | head: inode numbers in a real directory.