File metadata and permissions

File.stat

File.stat asks the system about a path and returns an object with the answer: size, mode, owner, the three times and the type. It raises if the path does not exist, which is the difference from File.exist?:

ruby -e 'File.write("a.txt", "x\n"); p File.stat("a.txt").size; p File.stat("a.txt").mtime.class'

2
Time

The type predicates are on File itself and are the short form: File.file?, File.directory?, File.symlink?, File.readable?. Each one stats the path, so a loop that asks twice does the work twice.

The mode in octal

The mode packs the file type and the permission bits into one integer, which is why it is printed in octal. A directory and a regular file:

ruby -e 'p File.stat(".").mode.to_s(8)'

"40755"

ruby -e 'File.write("a.txt", ""); File.chmod(0o644, "a.txt"); p File.stat("a.txt").mode.to_s(8)'

"100644"

The leading 4 is a directory and the 10 is a regular file. The last three digits are the permissions in the order owner, group, other: 755 is read-write-execute for the owner and read-execute for the rest, 644 is read-write for the owner and read for the rest. chmod takes the number with a 0o prefix, or a string such as "u+x".

A file that a program writes is created with the process umask applied, usually 644. Making it executable is an explicit chmod 0o755, which is what an install script does after it copies a command.

mtime and the other times

stat.mtime is when the contents last changed, atime when it was last read, and ctime when the metadata last changed. All three are Time objects, so the formatting methods work directly:

ruby -e 'File.write("a.txt", "x\n"); p File.mtime("a.txt").strftime("%Y-%m-%d %H:%M:%S")'

"2026-11-05 00:14:44"

File.mtime is the shortcut for File.stat(path).mtime. The timestamp has sub-second precision even when strftime shows whole seconds, which is what the next lesson watches.

Symlinks

A symlink is a path that points at another path. File.stat follows it and reports the target; File.lstat reports the link itself:

ruby -e 'File.write("a.txt", "x\n"); File.symlink("a.txt", "link.txt"); p File.symlink?("link.txt"); p File.stat("link.txt").size; p File.lstat("link.txt").size'

true
2
5

Two bytes for the target's contents, five for the name it stores. A cleanup script that walks a tree with lstat sees the link and does not descend into the target, which is what stops a loop when a link points at its own directory.

A directory report

The pieces together: a list of names, a stat per name, and a formatted line, run in a folder that holds a.txt with two bytes and b.txt with six. The widths come from lesson 90:

Dir.glob("*.txt").sort.each do |name| stat = File.stat(name) puts format("%-12s %5d %s", name, stat.size, stat.mtime.strftime("%Y-%m-%d %H:%M")) end

ruby report.rb

a.txt 2 2026-11-05 00:14 b.txt 6 2026-11-05 00:14

File.expand_path is the missing piece when a report must name the file it read. It turns a relative path into an absolute one, with ~ for the home folder:

ruby -e 'p File.expand_path("~")'

"/home/agent"

What to watch for

The mode is a number, not a permission API. To ask whether the current user may read a file, use File.readable?, which takes the owner and the group into account; the mode bits alone do not.

Times come from the file system and have a resolution that depends on it. A test that writes a file and compares a timestamp to Time.now can fail on a file system with one-second precision; compare with a tolerance, or compare the contents.

Next: Watching files for changes

Article author: Arthur Isaev

Banner image