sysadmin · difficulty ◆◆
stat — read a file's metadata, not its contents
ls -l shows you an abbreviation of a file. stat shows you the whole record.
`ls -l` said `-rw-r-----`, which is a 9-character summary of four numbers. stat prints the four numbers — and the 512-byte block count that actually explains your disk usage.
$ statWhat it does
stat asks the kernel for the metadata of one or more paths and prints it as a labelled block: size, allocated blocks, device, inode, hard link count, permissions in octal and symbolic form, owner and group, and the three timestamps (Access, Modify, Change — plus Birth when the filesystem and kernel support it). It is the same system call ls, find, tar and your editor call underneath; stat is just the tool that shows you the raw answer instead of a formatted guess. Give it `-f` and it switches target entirely, reporting the filesystem rather than the file: type, block size, total/free/available blocks and inode counts.
The three things only stat tells you
First, allocated blocks. `Size: 2122121` is the logical length; `Blocks: 4160` is what was actually reserved on disk, in 512-byte units — 2,129,920 bytes, slightly more than the file's size, because the filesystem allocated whole blocks. A sparse file shows the opposite and dramatic gap. Second, the octal permission value, shown as `(0640/-rw-r-----)` and available alone with `-c %a` or `-c %f`. Third, Change time. Modify (`%y`) is the last time file contents changed; Change (`%z`) is the last time the inode changed — a chmod, a chown, a rename or a link operation moves Change without touching Modify. When someone insists 'nobody edited that file', `%z` is the column that catches a permission fix.
Why scripts use -c and humans use the default
The default output is for reading. `-c %FORMAT` is for piping: it prints your format once per file, with a trailing newline, so `stat -c '%s %n' * | sort -rn | head` is a one-line size ranking and `stat -c '%a %U:%G %n' /etc/*` is a permissions audit. `--printf` is the same but interprets backslash escapes and omits the trailing newline, which is what you want when composing multi-field rows yourself. Format letters are case-paired throughout: lowercase is the compact or numeric form (owner UID), uppercase is the human form (owner name); `%y` human vs `%Y` epoch seconds, `%a` octal vs `%A` symbolic.
Example
$ stat /var/log/auth.log File: /var/log/auth.log
Size: 2122121 Blocks: 4160 IO Block: 4096 regular file
Device: 252,0 Inode: 6320758 Links: 1
Access: (0640/-rw-r-----) Uid: ( 103/ syslog) Gid: ( 4/ adm)
Access: 2026-10-04 09:01:15.390876634 +0200
Modify: 2026-10-04 09:04:05.113256070 +0200
Change: 2026-10-04 09:04:05.113256070 +0200
Birth: 2026-10-04 00:00:22.016109684 +0200Real output from this host, and it answers four questions ls -l cannot: the file is owned by uid 103 / gid 4 (not 'root'), the mode is exactly 0640, the timestamps carry nanoseconds, and Birth tells you the log was rotated at 00:00:22 today. Access is older than Modify — something read the file at 09:01 and syslog appended to it at 09:04.
$ stat -c '%s %n' /var/log/*.log | sort -rn | head -42122121 /var/log/auth.log
123308 /var/log/kern.log
74595 /var/log/cloud-init.log
61229 /var/log/bootstrap.logThe 'what is eating my /var/log' one-liner. -c '%s %n' emits size then name, one file per line, which sort -rn ranks numerically — no du, no recursion, no waiting on a tree walk. Real output. Swap %s for %b and you get allocated 512-byte blocks instead, which is the number that matters on a sparse or recently-truncated file.
$ stat -c '%a %U:%G %n' /etc/passwd /etc/shadow /etc/sudoers644 root:root /etc/passwd
640 root:shadow /etc/shadow
440 root:root /etc/sudoersA permissions audit in one line, real output from this box. %a gives the octal mode as a bare number, %U the owner name, %G the group name. Feed the same invocation a glob or a find -exec and you have a CIS-style check: anything world-writable or not owned by root shows up immediately. Note the three files legitimately differ — 644, 640, 440 — which is exactly what you want to be able to see at a glance.
$ stat -c 'link: %N' /bin && stat -L -c 'target: %n type=%F mode=%A' /binlink: '/bin' -> 'usr/bin'
target: /bin type=directory mode=drwxr-xr-xstat does not follow symlinks by default — it reports the link itself, which is why the default listing for /bin shows `Size: 7` (the length of the string 'usr/bin') and mode 0777. Add -L and you get the target's metadata instead: a directory, mode 0755. %N prints the quoted name with the arrow; without -L that is the cleanest way to see what a symlink points at. Real output from this host, where /bin -> usr/bin.
$ stat -f -c 'fs=%T block=%S total=%b free=%f avail=%a' /fs=ext2/ext3 block=4096 total=119243288 free=20130105 avail=15006882The same binary, now pointed at a filesystem: total/free/available blocks and the block size. Real output this morning — 454.9 GiB total, 76.8 GiB free but only 57.2 GiB available, the 19.5 GiB difference being the ext4 5% reserved-for-root pool, which is why `df -h` shows 58G and a non-root process gets 'No space left on device' while df still looks roomy. Note %T reports the kernel's ext4 superblock identifier as `ext2/ext3` — check with mount or lsblk if you need the real driver.
Common flags
- -c FORMAT
- Print your format string instead of the default block, one newline per file. The only mode worth using in scripts.
- --printf FORMAT
- Like -c but interprets backslash escapes and suppresses the trailing newline — for building your own multi-field rows.
- -f
- Report the filesystem that holds the path instead of the path itself: type, block size, total/free/available blocks and inodes.
- -L
- Dereference: report the target of a symlink instead of the link. Without it, /bin shows itself, 7 bytes long and mode 0777.
- -t
- Terse output, one line of bare fields with no labels. Stable for awk field-splitting.
- --cached=MODE
- always | never | default — how aggressively to use cached attributes on network filesystems. Default is 'default'.
- -c %w
- File birth time; prints '-' when the filesystem or kernel cannot supply it, which is the honest answer rather than a fabricated date.
History
Stat is older than the command
The name comes from stat(2), the system call that has always been the only way to ask a Unix kernel about a file. The command-line tool arrived last: coreutils' NEWS records 'new programs: link, unlink, and stat' in the fileutils-4.1.9 release, and the same release notes show the interface still being argued over a version later — 'stat: remove support for --secure/-s option and related %S and %C format specs' and 'stat: rename --link/-l to --dereference/-L' both landed in 4.1.10. So the wrapper you type today is a 2000s invention, while the record it prints was defined in Version 7 Unix.
Birth time had to wait for a new system call
For decades the three timestamps were all a Unix file had, because the on-disk inode simply had no field for creation time. Linux added one in kernel 4.11 via the new statx(2) call, and coreutils 8.31 (2019) documented the rule: 'stat now prints file creation time when supported by the file system, on GNU Linux systems with glibc >= 2.28 and kernel >= 4.11.' That is why `Birth:` sometimes prints a real nanosecond timestamp and sometimes prints `-` — it is a genuine capability probe, not a bug, and a file copied onto a filesystem has a Birth that reflects the copy, never the original.
Fun facts
Pros & cons
pros
- + Every field exact and unrounded: octal mode, numeric UID/GID, nanosecond timestamps, allocated vs logical size — no ls abbreviations to decode
- + One binary covers two questions: -c formats a single path into anything a script needs, -f reports the whole filesystem's space and inode budget
- + Format letters are systematically paired (lowercase numeric, uppercase human), so %s/%n piped into sort or awk needs no text surgery
cons
- − Output is verbose for one field — you either read a nine-line block or memorise format specifiers, and there is no pleasant middle
- − Not portable in behaviour: BSD/macOS stat uses -f for its own different format language and has no -c at all, so a script written here breaks there
- − Birth time and %T filesystem-type output depend on kernel, glibc and filesystem support, so the same command can print a timestamp, a dash, or `ext2/ext3` for a filesystem the driver calls ext4
Takeaways
- 1`stat -c '%s %n' dir/* | sort -rn | head` ranks files by size without walking a tree
- 2Read `Blocks` for real disk usage — Size is the logical length, and sparse files make the two differ by orders of magnitude
- 3If `%y` (Modify) is older than `%z` (Change), someone changed permissions, ownership or the file's name after the last edit
- 4Reuse exact metadata instead of retyping it: `chmod $(stat -c '%a' ref) target`, `chown $(stat -c '%U:%G' ref) target`
- 5Default stat reports a symlink as itself — add `-L` when you want the target, and remember `-f` reports the filesystem, not the file