Linux stat Command Deep Dive: Complete Metadata Extraction from Inodes to Timestamps
Linux stat Command Deep Dive: Complete Metadata Extraction from Inodes to Timestamps#
The stat command is one of the most underrated tools in Linux. Most people rely on ls -l to check file information, but stat reveals the underlying truths that ls can’t show.
The Core of stat: inode Metadata#
The essence of stat is reading a file’s inode (index node). In Linux filesystems, each file has a unique inode that stores all information except the filename and actual data.
stat example.txt
Output looks like this:
File: example.txt
Size: 1024 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 131074 Links: 1
Access: (0644/-rw-r--r--) Uid: (1000/user) Gid: (1000/user)
Access: 2025-05-12 10:30:00.123456789 +0800
Modify: 2025-05-12 09:15:00.987654321 +0800
Change: 2025-05-12 09:15:00.987654321 +0800
Birth: -
Key Fields Explained#
- Size: Actual file size in bytes
- Blocks: Number of 512-byte blocks allocated (not simply file size / 512, includes metadata and preallocation)
- IO Block: Optimal I/O block size for the filesystem (usually 4096 bytes)
- Inode: Inode number, unique within the filesystem
- Links: Hard link count
Three Types of Timestamps: atime, mtime, ctime#
This is the most valuable part of stat, and many people confuse these three:
Access Time (atime)#
The time the file was last read. Note these pitfalls:
# Reading a file updates atime
cat file.txt
# But modern systems use relatime by default, only updating when mtime is earlier than atime
# This is a performance optimization to avoid frequent disk writes
Performance tuning in /etc/fstab:
noatime: Completely disable atime updates (better performance)relatime: Relative time updates (default)strictatime: Update on every access (poor performance)
Modify Time (mtime)#
The time the file content was last modified. This is what ls -l shows by default.
echo "new content" >> file.txt # Updates mtime and ctime
Change Time (ctime)#
The time the file metadata was last changed. Note it’s “changed” not “modified”:
chmod 755 file.txt # Only updates ctime
chown user file.txt # Only updates ctime
ln file.txt hardlink # Updates ctime (link count changed)
Key differences:
- Modifying file content: both mtime and ctime update
- Changing permissions/ownership: only ctime updates
- You cannot directly modify ctime, except at the filesystem level
Birth Time (Creation Time)#
Most Linux filesystems (ext4, xfs) support creation time, but stat shows - because GNU coreutils’ stat version is older. Use debugfs instead:
debugfs -R 'stat <inode_number>' /dev/sda1
Under the Hood: System Calls#
The stat command calls the stat() system call:
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf); // Don't follow symlinks
int fstat(int fd, struct stat *statbuf); // Via file descriptor
Key fields in struct stat:
struct stat {
dev_t st_dev; // Device ID
ino_t st_ino; // Inode number
mode_t st_mode; // File type and permissions
nlink_t st_nlink; // Number of hard links
uid_t st_uid; // User ID
gid_t st_gid; // Group ID
dev_t st_rdev; // Special device ID
off_t st_size; // File size (bytes)
blksize_t st_blksize; // I/O block size
blkcnt_t st_blocks; // Number of 512-byte blocks allocated
time_t st_atime; // Access time
time_t st_mtime; // Modification time
time_t st_ctime; // Status change time
};
Formatted Output: The Art of -c Flag#
stat -c allows custom output formatting, essential for script automation:
# Show only file size
stat -c "%s" file.txt
# Show inode and filename
stat -c "%i %n" file.txt
# Show permissions (octal)
stat -c "%a %n" file.txt
# Show owner username
stat -c "%U %G %n" file.txt
# Custom time format
stat -c "%y %n" file.txt # ISO format mtime
stat -c "%.10y %n" file.txt # Only date part (first 10 chars)
Practical Format Specifiers#
| Format | Meaning | Example |
|---|---|---|
%s |
File size (bytes) | 1024 |
%b |
Blocks allocated | 8 |
%i |
Inode number | 131074 |
%n |
Filename | file.txt |
%N |
Quoted filename | ‘file.txt’ |
%a |
Octal permissions | 644 |
%A |
Symbolic permissions | -rw-r–r– |
%U |
Username | user |
%G |
Group name | user |
%y |
mtime | 2025-05-12 10:30:00.123456789 +0800 |
%z |
ctime | 2025-05-12 10:30:00.123456789 +0800 |
%x |
atime | 2025-05-12 10:30:00.123456789 +0800 |
Filesystem Information: -f Flag#
stat -f shows filesystem-level information:
stat -f /home
Output:
File: "/home"
ID: 12345678-90ab-cdef-1234-567890abcdef Namelen: 255
Type: ext2/ext3/ext4
Block size: 4096
Blocks: Total: 26214400 Free: 20971520 Available: 19922944
Inodes: Total: 16777216 Free: 15938355
Key fields:
- Block size: Filesystem block size (affects storage efficiency)
- Total/Free/Available: Block statistics
- Inodes: Total and free inodes (inode exhaustion prevents creating new files)
Real-World Scenarios#
1. Find Oldest Files (by Creation Time)#
find /path -type f -printf "%C@ %p\n" | sort -n | head -5
2. Detect File Tampering#
# Backup critical file's ctime
stat -c "%z" /etc/passwd > /var/log/passwd.ctime
# Periodic check
if [ "$(stat -c "%z" /etc/passwd)" != "$(cat /var/log/passwd.ctime)" ]; then
echo "Warning: /etc/passwd has been modified!"
fi
3. Batch File Size Calculation#
# Sum all .log files in current directory
find . -name "*.log" -exec stat -c "%s" {} + | awk '{sum+=$1} END {print sum}'
4. Real File Size Excluding Symlinks#
# Only count regular files, ignore symlinks
find . -type f -exec stat -c "%s" {} + | awk '{sum+=$1} END {print sum}'
5. Monitor Inode Usage#
# Check filesystem inode usage
df -i | grep -v Filesystem | while read fs total used free percent mount; do
echo "Mount: $mount | Inode Usage: $percent"
done
stat vs ls -l: When to Use Which?#
| Scenario | Recommended Tool | Reason |
|---|---|---|
| Check permissions | ls -l |
More intuitive |
| Check file size | ls -lh |
Human readable |
| Full timestamps | stat |
Nanosecond precision |
| All three times | stat |
ls only shows mtime |
| Inode info | stat |
ls -i only shows number |
| Script field extraction | stat -c |
Easier to parse |
| Filesystem info | stat -f |
ls doesn’t support |
Common Pitfalls#
1. Timezone Issues#
stat shows local time, but scripts may need UTC:
# Get UTC timestamp
stat -c "%Y" file.txt | xargs -I {} date -d @{} -u "+%Y-%m-%d %H:%M:%S"
2. Symlink Handling#
By default, stat follows symlinks:
ln -s target link
stat link # Shows target's info
stat -L link # Same as above (-L is default)
3. Block Count Misconception#
st_blocks is in 512-byte units, not filesystem block size:
# File size 1024 bytes
stat -c "%s %b" file.txt
# Output: 1024 8
# Actual usage: 8 * 512 = 4096 bytes (one 4KB block)
Web Implementation: Node.js File Metadata#
Implementing stat-like functionality in Node.js:
const fs = require('fs').promises;
async function getFileStats(filePath) {
try {
const stats = await fs.stat(filePath);
return {
size: stats.size,
mode: stats.mode.toString(8).slice(-4), // Octal permissions
uid: stats.uid,
gid: stats.gid,
atime: stats.atime.toISOString(),
mtime: stats.mtime.toISOString(),
ctime: stats.ctime.toISOString(),
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
isSymbolicLink: stats.isSymbolicLink(),
ino: stats.ino, // Inode number
blocks: stats.blocks,
blksize: stats.blksize
};
} catch (error) {
throw new Error(`Cannot stat ${filePath}: ${error.message}`);
}
}
// Usage example
getFileStats('/path/to/file').then(console.log);
Browser limitations: Browsers can’t directly access filesystem metadata. File API only provides lastModified (similar to mtime).
Summary#
The stat command is a window into the Linux filesystem internals. It exposes the complete inode structure, revealing the truth behind files. Remember the three timestamp types, master the -c format syntax, and you’ll be equipped for script automation and troubleshooting.
Next time you need precise file metadata, don’t rely on ls -l. Use stat to get the complete picture.
Related Tools:
- Linux ls Command - Directory listing
- Linux file Command - File type detection
- Linux find Command - File search tool