123456789_123456789_123456789_123456789_123456789_

Class: File::Stat

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
self, ::Comparable
Inherits: Object
Defined in: file.c,
file.c

Overview

A File::Stat object contains information about an entry in the filesystem.

Each of these methods returns a new File::Stat object. the first three follow symbolic links; the others don't:

Snapshot

A new File::Stat object takes an immediate "snapshot" of the filesystem entry at the given 'path'; the snapshot is never updated, regardless of changes in the entry (even its deletion):

filepath = '/tmp/t.tmp'
File.stat(filepath) # Raises Errno::ENOENT: No such file or directory.
File.write(filepath, 'foo')
stat = File.stat(filepath)
stat.birthtime # => 2026-10-03 12:19:23.723062465 -0500
File.delete(filepath)
stat.birthtime # => 2026-10-03 12:19:23.723062465 -0500

Filesystem Dependencies

Methods in a File::Stat object may return filesystem-dependent values, and not all values are meaningful on all filesystems; for example, #blocks returns nil on a Windows filesystem, but returns an integer on others.

See also Kernel.test.

Class Method Summary

Instance Attribute Summary

Instance Method Summary

::Comparable - Included

#<

Returns whether self is "less than" other; equivalent to (self <=> other) < 0:

#<=

Returns whether self is "less than or equal to" other; equivalent to (self <=> other) <= 0:

#==

Compares two objects based on the receiver's #<=> method, returning true if it returns 0.

#>

Returns whether self is "greater than" other; equivalent to (self <=> other) > 0:

#>=

Returns whether self is "greater than or equal to" other; equivalent to (self <=> other) >= 0:

#between?

Returns false if obj #<=> min is less than zero or if obj #<=> max is greater than zero, true otherwise.

#clamp

Returns an object of the same type as self.

Constructor Details

.new(path) ⇒ Stat

Returns a new File::Stat object containing asnapshot of the filesystem entry at the given File.path:

File::Stat.new('/etc/passwd')
File::Stat.new('/tmp')
File::Stat.new('nosuch') # Raises Errno::ENOENT: No such file or directory.
[ GitHub ]

  
# File 'file.c', line 7107

static VALUE
rb_stat_init(VALUE obj, VALUE fname)
{
    rb_io_stat_data st;

    FilePathValue(fname);
    fname = rb_str_encode_ospath(fname);
    if (STATX(StringValueCStr(fname), &st, STATX_ALL) == -1) {
        rb_sys_fail_path(fname);
    }

    struct rb_stat *rb_st;
    TypedData_Get_Struct(obj, struct rb_stat, &stat_data_type, rb_st);

    rb_st->stat = st;
    rb_st->initialized = true;

    return Qnil;
}

Instance Attribute Details

#blockdev? ⇒ Boolean (readonly)

Returns whether the entry in thesnapshot in self is a block device (i.e., a direct-access device):

File.stat('/dev/nvme0n1').blockdev? # => true
File.stat('/dev/loop0').blockdev?   # => true
File.stat('/dev/tty').blockdev?     # => false
File.stat('/dev/null').blockdev?    # => false

The returned value is filesystem-dependent; returns false on Windows or MacOS.

[ GitHub ]

  
# File 'file.c', line 7333

static VALUE
rb_stat_b(VALUE obj)
{
#ifdef S_ISBLK
    if (S_ISBLK(get_stat(obj)->ST_(mode))) return Qtrue;

#endif
    return Qfalse;
}

#chardev? ⇒ Boolean (readonly)

Returns whether the entry in thesnapshot in self is a character device (i.e., a sequential-access device):

File.stat('/dev/tty').chardev?     # => true
File.stat('/dev/null').chardev?    # => true
File.stat('/dev/nvme0n1').chardev? # => false
File.stat('/dev/loop0').chardev?   # => false

The returned value is filesystem-dependent; on Windows, always false.

[ GitHub ]

  
# File 'file.c', line 7362

static VALUE
rb_stat_c(VALUE obj)
{
    if (S_ISCHR(get_stat(obj)->ST_(mode))) return Qtrue;

    return Qfalse;
}

#directory? ⇒ Boolean (readonly)

Returns whether the entry in thesnapshot in self is a directory:

File.stat('/etc').directory?        # => true
File.stat('/etc/passwd').directory? # => false
[ GitHub ]

  
# File 'file.c', line 7206

static VALUE
rb_stat_d(VALUE obj)
{
    if (S_ISDIR(get_stat(obj)->ST_(mode))) return Qtrue;
    return Qfalse;
}

#executable? ⇒ Boolean (readonly)

Returns whether the filesystem entry represented by self isexecutable by the effective owner/group of the current process:

File.stat('/bin/ruby').executable?   # => true
File.stat('/etc').executable?        # => true
File.stat('/etc/passwd').executable? # => false

Raises Errno::ENOENT if the entry does not exist.

Note that some filesystem settings may cause this method to return true even though the entry is not executable by the effective owner/group.

[ GitHub ]

  
# File 'file.c', line 7690

static VALUE
rb_stat_x(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (geteuid() == 0) {
        return RBOOL(st->ST_(mode) & S_IXUGO);
    }
#endif
#ifdef S_IXUSR
    if (rb_stat_owned(obj))
        return RBOOL(st->ST_(mode) & S_IXUSR);
#endif
#ifdef S_IXGRP
    if (rb_stat_grpowned(obj))
        return RBOOL(st->ST_(mode) & S_IXGRP);
#endif
#ifdef S_IXOTH
    if (!(st->ST_(mode) & S_IXOTH)) return Qfalse;
#endif
    return Qtrue;
}

#executable_real? ⇒ Boolean (readonly)

Like Pathname#executable?, but checks against the real owner/group instead of the effective owner/group.

[ GitHub ]

  
# File 'file.c', line 7723

static VALUE
rb_stat_X(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (getuid() == 0) {
        return RBOOL(st->ST_(mode) & S_IXUGO);
    }
#endif
#ifdef S_IXUSR
    if (rb_stat_rowned(obj))
        return RBOOL(st->ST_(mode) & S_IXUSR);
#endif
#ifdef S_IXGRP
    if (rb_group_member(get_stat(obj)->ST_(gid)))
        return RBOOL(st->ST_(mode) & S_IXGRP);
#endif
#ifdef S_IXOTH
    if (!(st->ST_(mode) & S_IXOTH)) return Qfalse;
#endif
    return Qtrue;
}

#file? ⇒ Boolean (readonly)

Returns whether self represents a filesystem entry that exists and is a regular file; see File.ftype:

File.stat('/etc/passwd').file? # => true
File.stat('/etc').file?        # => false
[ GitHub ]

  
# File 'file.c', line 7763

static VALUE
rb_stat_f(VALUE obj)
{
    if (S_ISREG(get_stat(obj)->ST_(mode))) return Qtrue;
    return Qfalse;
}

#grpowned?(path) ⇒ Boolean (readonly)

Returns false on Windows.

On other systems, returns whether the effective group id of the calling process is the owner of thesnapshot in self:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.stat(filepath).grpowned?      # => true
File.stat('/etc').grpowned?        # => false
File.stat('/etc/passwd').grpowned? # => false
File.delete(filepath)              # Clean up.
[ GitHub ]

  
# File 'file.c', line 7431

static VALUE
rb_stat_grpowned(VALUE obj)
{
#ifndef _WIN32
    if (rb_group_member(get_stat(obj)->ST_(gid))) return Qtrue;
#endif
    return Qfalse;
}

#owned? ⇒ Boolean (readonly)

Returns whether the owner in thesnapshot in self is the owner of the current process:

dirpath = '/tmp/dir'
Dir.mkdir(dirpath)
filepath = File.join(dirpath, 't.tmp') # => "/tmp/dir/t.tmp"
File.write(filepath, 'foo')
dirstat = File.stat(dirpath)           # Snapshot of directory.
filestat = File.stat(filepath)         # Snapshot of file.
dirstat.owned?                         # => true
filestat.owned?                        # => true
FileUtils.rm_rf(dirpath)               # Clean up.
dirstat.owned?                         # => true  # Snapshot not updated.
filestat.owned?                        # => true  # Snapshot not updated.
[ GitHub ]

  
# File 'file.c', line 7395

static VALUE
rb_stat_owned(VALUE obj)
{
    if (get_stat(obj)->ST_(uid) == geteuid()) return Qtrue;
    return Qfalse;
}

#pipe? ⇒ Boolean (readonly)

Returns whether the entry at the path in self is a pipe:

pipepath = '/tmp/pipe'
File.mkfifo(pipepath)                       # Make pipe.
stat = File.stat(pipepath)                  # Take snapshot.
stat.pipe?                       # => true  # In snapshot.
File.delete(pipepath)                       # Clean up.
stat.pipe?                       # => true  # Snapshot not updated.
File.stat('/etc').pipe?          # => false
File.stat('/etc/passwd').pipe?   # => false
[ GitHub ]

  
# File 'file.c', line 7234

static VALUE
rb_stat_p(VALUE obj)
{
#ifdef S_IFIFO
    if (S_ISFIFO(get_stat(obj)->ST_(mode))) return Qtrue;

#endif
    return Qfalse;
}

#readable? ⇒ Boolean (readonly)

Returns whether the entry represented by self exists and is readable by the owner and group of the current process; seePermissions:

path = '/tmp/secret.txt'
File.write(path, 'foo')
File.stat(path).readable? # => true
File.chmod(0o000, path)
File.stat(path).readable? # => false
File.delete(path)         # Clean up.
[ GitHub ]

  
# File 'file.c', line 7461

static VALUE
rb_stat_r(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (geteuid() == 0) return Qtrue;
#endif
#ifdef S_IRUSR
    if (rb_stat_owned(obj))
        return RBOOL(st->ST_(mode) & S_IRUSR);
#endif
#ifdef S_IRGRP
    if (rb_stat_grpowned(obj))
        return RBOOL(st->ST_(mode) & S_IRGRP);
#endif
#ifdef S_IROTH
    if (!(st->ST_(mode) & S_IROTH)) return Qfalse;
#endif
    return Qtrue;
}

#readable_real? ⇒ Boolean (readonly)

Like #readable?, but checks against the real owner and group ids instead of the effective ids.

[ GitHub ]

  
# File 'file.c', line 7493

static VALUE
rb_stat_R(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (getuid() == 0) return Qtrue;
#endif
#ifdef S_IRUSR
    if (rb_stat_rowned(obj))
        return RBOOL(st->ST_(mode) & S_IRUSR);
#endif
#ifdef S_IRGRP
    if (rb_group_member(get_stat(obj)->ST_(gid)))
        return RBOOL(st->ST_(mode) & S_IRGRP);
#endif
#ifdef S_IROTH
    if (!(st->ST_(mode) & S_IROTH)) return Qfalse;
#endif
    return Qtrue;
}

#setgid? ⇒ Boolean (readonly)

Returns whether the setgid bit is set in thespecial bits for the entry represented in self:

path = '/tmp/t.tmp'
File.write(path, 'foo')
stat = File.stat(path)   # Take a snapshot.
stat.setgid?             # => false
stat.mode.to_s(8)        # => "100664"
File.chmod(0o2644, path) # Set the bit; stat snapshot unchanged.
stat.setgid?             # => false
stat.mode.to_s(8)        # => "100664"
stat = File.stat(path)   # Fresh stat; snapshot changed.
stat.setgid?             # => true
stat.mode.to_s(8)        # => "102644"
File.delete(path)        # Clean up.

On Windows, the bit is never set; the method always returns false.

[ GitHub ]

  
# File 'file.c', line 7914

static VALUE
rb_stat_sgid(VALUE obj)
{
#ifdef S_ISGID
    if (get_stat(obj)->ST_(mode) & S_ISGID) return Qtrue;
#endif
    return Qfalse;
}

#setuid? ⇒ Boolean (readonly)

Returns whether the setuid bit is set in thespecial bits for the entry represented in self:

path = '/tmp/t.tmp'
File.write(path, 'foo')
stat = File.stat(path)   # Take snapshot; bit not set.
stat.setuid?             # => false
stat.mode.to_s(8)        # => "100664"
File.chmod(0o4644, path) # Set the bit; snapshot not updated.
stat.setuid?             # => false
stat.mode.to_s(8)        # => "100664"
stat = File.stat(path)   # Fresh snapshot.
stat.setuid?             # => true
stat.mode.to_s(8)        # => "104644"
File.delete(path)        # Clean up.

On Windows, the bit is never set; the method always returns false.

[ GitHub ]

  
# File 'file.c', line 7877

static VALUE
rb_stat_suid(VALUE obj)
{
#ifdef S_ISUID
    if (get_stat(obj)->ST_(mode) & S_ISUID) return Qtrue;
#endif
    return Qfalse;
}

#size ⇒ Integer (readonly)

Returns the size (in bytes) of the entry in thesnapshot in self:

File.stat('/etc/passwd').size # => 3092   # Regular file.
File.stat('/etc').size        # => 12288  # Directory.
#### When the file size changes.
filepath = '/tmp/t.tmp'
file = File.new(filepath, 'w+')
file.write('foo')
file.size                     # => 3
stat0 = File.stat(filepath)               # Take snapshot.
stat0.size                    # => 3
file.write('bar')                         # Change file size.
file.size                     # => 6
stat0.size                    # => 3      # Snapshot not updated.
stat1 = File.stat(filepath)               # Take new snapshot.
stat1.size                    # => 6      # Snapshot has updated size.
File.delete(filepath)                     # Clean up.
[ GitHub ]

  
# File 'file.c', line 1097

static VALUE
rb_stat_size(VALUE self)
{
    return OFFT2NUM(get_stat(self)->ST_(size));
}

#size? ⇒ Boolean (readonly)

Returns the size in bytes of the entry in self if the entry exists and has non-zero size, nil otherwise:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.size(filepath) # => 3
stat = File.stat(filepath)    # Take snapshot.
stat.size?          # => 3    # Non-zero size.
File.write(filepath, '')
File.size(filepath) # => 0
stat.size?          # => 3    # Snapshot unchanged.
stat = File.stat(filepath)    # Take new snapshot.
stat.size?          # => nil  # Zero size
File.delete(filepath)         # Clean up.
[ GitHub ]

  
# File 'file.c', line 7840

static VALUE
rb_stat_s(VALUE obj)
{
    rb_off_t size = get_stat(obj)->ST_(size);

    if (size == 0) return Qnil;
    return OFFT2NUM(size);
}

#socket? ⇒ Boolean (readonly)

Returns whether entry in thesnapshot in self is a socket:

require 'socket'
socket_path = '/tmp/socket'
server = UNIXServer.new(socket_path)
stat = File.stat(socket_path)         # Take snapshot.
stat.socket?              # => true
server.close                          # Clean up.
File.delete(socket_path)              # Clean up.
stat.socket?              # => true   # Snapshot not updated.
File.stat('/tmp').socket? # => false  # Not a socket.
[ GitHub ]

  
# File 'file.c', line 7303

static VALUE
rb_stat_S(VALUE obj)
{
#ifdef S_ISSOCK
    if (S_ISSOCK(get_stat(obj)->ST_(mode))) return Qtrue;

#endif
    return Qfalse;
}

#sticky? ⇒ Boolean (readonly)

Returns whether the sticky bit is set in thespecial bits for self:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
stat = File.stat(filepath)
stat.sticky?                # => false
stat.mode.to_s(8)           # => "100664"
File.chmod(01644, filepath) # => 1        # Stat unchanged.
stat.sticky?                # => false
stat.mode.to_s(8)           # => "100664"
stat = File.stat(filepath)                # Fresh stat.
stat.sticky?                # => true
stat.mode.to_s(8)           # => "101644"
File.delete(filepath)                     # Clean up.

Returns false on Windows.

[ GitHub ]

  
# File 'file.c', line 7951

static VALUE
rb_stat_sticky(VALUE obj)
{
#ifdef S_ISVTX
    if (get_stat(obj)->ST_(mode) & S_ISVTX) return Qtrue;
#endif
    return Qfalse;
}

#symlink? ⇒ Boolean (readonly)

Returns whether the entry in self (seeSnapshot) is asymbolic link:

filepath = '/etc/passwd'
linkpath = '/tmp/foo'
File.symlink(filepath, linkpath)
File.stat(filepath).symlink?  # => false
File.lstat(filepath).symlink? # => false
stat = File.stat(linkpath)    # Snapshot with stat follows link.
stat.symlink?                 # => false
lstat = File.lstat(linkpath)  # Snapshot with lstat does not follow link.
lstat.symlink?                # => true
File.delete(linkpath)         # Clean up.
#### Snapshots are unchanged, even when link deleted.
stat.symlink?                 # => false
lstat.symlink?                # => true
[ GitHub ]

  
# File 'file.c', line 7271

static VALUE
rb_stat_l(VALUE obj)
{
#ifdef S_ISLNK
    if (S_ISLNK(get_stat(obj)->ST_(mode))) return Qtrue;
#endif
    return Qfalse;
}

#world_readable? ⇒ Boolean (readonly)

Returns the integerpermissions for the entry in thesnapshot in self if those permissions allow reading by others; otherwise, returns nil:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')               # Create file.
File.chmod(0o000, filepath)               # Make file unreadable.
stat0 = File.stat(filepath)               # Take snapshot.
stat0.world_readable?         # => nil    # Snapshot shows not world-readable.
File.chmod(0o644, filepath)               # Make file world-readable.
stat0.world_readable?         # => nil    # Snapshot not updated.
stat1 = File.stat(filepath)               # Take new snapshot.
stat1.world_readable?.to_s(8) # => "644"  # Snapshot shows world-readable.
File.delete(filepath)                     # Clean up.
stat1.world_readable?.to_s(8) # => "644"  # Snapshot not updated.
[ GitHub ]

  
# File 'file.c', line 7541

static VALUE
rb_stat_wr(VALUE obj)
{
#ifdef S_IROTH
    rb_io_stat_data *st = get_stat(obj);
    if ((st->ST_(mode) & (S_IROTH)) == S_IROTH) {
        return UINT2NUM(st->ST_(mode) & (S_IRUGO|S_IWUGO|S_IXUGO));
    }
#endif
    return Qnil;
}

#world_writable? ⇒ Boolean (readonly)

If the entry in self exists and is writable by others, returns the integerpermissions for the entry; otherwise, returns nil:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.stat(filepath).world_writable?         # => nil   # Not world-writable.
File.chmod(0o777, filepath)                            # Make world-writable.
File.stat(filepath).world_writable?.to_s(8) # => "777" # World-writable.
File.delete(filepath)                                  # Clean up.
File.stat('/tmp').world_writable?.to_s(8)   # => "777" # Directory.
[ GitHub ]

  
# File 'file.c', line 7656

static VALUE
rb_stat_ww(VALUE obj)
{
#ifdef S_IWOTH
    rb_io_stat_data *st = get_stat(obj);
    if ((st->ST_(mode) & (S_IWOTH)) == S_IWOTH) {
        return UINT2NUM(st->ST_(mode) & (S_IRUGO|S_IWUGO|S_IXUGO));
    }
#endif
    return Qnil;
}

#writable? ⇒ Boolean (readonly)

Returns whether the entry at the path in self exists and is writable by the effective owner and group in the current process:

filepath = '/tmp/secret.txt'
File.write(filepath, 'foo')
File.stat(filepath).writable? # => true   # Writable.
File.chmod(0o000, filepath)               # Make non-writable.
File.stat(filepath).writable? # => false  # Not writable.
File.delete(filepath)                     # Clean up.
File.stat('/etc').writable?   # => false  # Directory.

Note that filesystem security features may cause this method to return true even when the file is not writable by the effective owner and group.

[ GitHub ]

  
# File 'file.c', line 7576

static VALUE
rb_stat_w(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (geteuid() == 0) return Qtrue;
#endif
#ifdef S_IWUSR
    if (rb_stat_owned(obj))
        return RBOOL(st->ST_(mode) & S_IWUSR);
#endif
#ifdef S_IWGRP
    if (rb_stat_grpowned(obj))
        return RBOOL(st->ST_(mode) & S_IWGRP);
#endif
#ifdef S_IWOTH
    if (!(st->ST_(mode) & S_IWOTH)) return Qfalse;
#endif
    return Qtrue;
}

#writable_real? ⇒ Boolean (readonly)

Like File::Stat.writable?, but checks against the real owner and group instead of the effective owner and group.

Note that filesystem security features may cause this method to return true even when the entry in self is not writable by the real owner and group.

[ GitHub ]

  
# File 'file.c', line 7611

static VALUE
rb_stat_W(VALUE obj)
{
    rb_io_stat_data *st = get_stat(obj);

#ifdef USE_GETEUID
    if (getuid() == 0) return Qtrue;
#endif
#ifdef S_IWUSR
    if (rb_stat_rowned(obj))
        return RBOOL(st->ST_(mode) & S_IWUSR);
#endif
#ifdef S_IWGRP
    if (rb_group_member(get_stat(obj)->ST_(gid)))
        return RBOOL(st->ST_(mode) & S_IWGRP);
#endif
#ifdef S_IWOTH
    if (!(st->ST_(mode) & S_IWOTH)) return Qfalse;
#endif
    return Qtrue;
}

#zero? ⇒ Boolean (readonly)

Returns whether the entry at the path in self has size zero.

The entry may be a file:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.stat(filepath).zero? # => false
File.truncate(filepath, 0)
File.stat(filepath).zero? # => true
File.delete(filepath)     # Clean up.

The entry may be a directory:

dirpath = '/tmp/foo'
Dir.mkdir(dirpath)
stat = File.stat(dirpath)
#### Size is filesystem-dependent; may or may not be zero.
stat.size                              # => 4096
stat.zero?                             # => false
filepath = File.join(dirpath, 't.tmp') # => "/tmp/foo/t.tmp"
File.write(filepath, 'foo')
stat = File.stat(dirpath)
stat.size                              # => 4096
stat.zero?                             # => false
FileUtils.rm_rf(dirpath)               # Clean up.
[ GitHub ]

  
# File 'file.c', line 7808

static VALUE
rb_stat_z(VALUE obj)
{
    if (get_stat(obj)->ST_(size) == 0) return Qtrue;
    return Qfalse;
}

Instance Method Details

#<=>(other) ⇒ 1, ...

Compares thesnapshots in self and other, by comparing their modification times self.mtime and other.mtime.

Returns:

  • -1, if self.mtime is earlier.
  • 0, if the two values are equal.
  • 1, if self.mtime is later.
  • nil, if other is not a File::Stat object.

Examples:

stat0 = File.stat('/etc')
stat1 = File.stat('/tmp')
stat0.mtime         # => 2026-10-02 07:52:03.151556038 -0500
stat1.mtime         # => 2026-10-03 12:22:25.719215899 -0500
stat0 <=> stat1     # => -1
stat0 <=> stat0.dup # => 0
stat1 <=> stat0     # => 1
stat0 <=> :foo      # => nil

Class File::Stat includes module Comparable, each of whose methods uses File::Stat#<=> for comparison.

[ GitHub ]

  
# File 'file.c', line 646

static VALUE
rb_stat_cmp(VALUE self, VALUE other)
{
    if (rb_obj_is_kind_of(other, rb_obj_class(self))) {
        stat_timestamp ts1 = statx_mtimespec(get_stat(self));
        stat_timestamp ts2 = statx_mtimespec(get_stat(other));
        if (ts1.tv_sec == ts2.tv_sec) {
            if (ts1.tv_nsec == ts2.tv_nsec) return INT2FIX(0);
            if (ts1.tv_nsec < ts2.tv_nsec) return INT2FIX(-1);
            return INT2FIX(1);
        }
        if (ts1.tv_sec < ts2.tv_sec) return INT2FIX(-1);
        return INT2FIX(1);
    }
    return Qnil;
}

#atime ⇒ Time

Returns a new ::Time object containing the access time of thesnapshot in self. See File System Timestamps.

Access time for a file is established when it is created, and may be updated when the file content is read:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.atime(filepath)        # => 2026-10-04 14:58:58.805179514 -0500
stat0 = File.stat(filepath) # Take snapshot.
stat0.atime                 # => 2026-10-04 14:58:58.805179514 -0500
File.read(filepath)         # Updates file atime.
File.atime(filepath)        # => 2026-10-04 14:59:47.707059351 -0500
stat0.atime                 # => 2026-10-04 14:58:58.805179514 -0500 # Not updated.
stat1 = File.stat(filepath) # Take new snapshot.
stat1.atime                 # => 2026-10-04 14:59:47.707059351 -0500 # Updated.
File.delete(filepath)       # Clean up.

Access time for a directory is established when it is created, and may be updated when its entries are read:

dirpath = '/tmp/dir'
Dir.mkdir(dirpath)
File.atime(dirpath)        # => 2026-10-04 15:07:26.194548647 -0500
stat0 = File.stat(dirpath) # Take snapshot.
stat0.atime                # => 2026-10-04 15:07:26.194548647 -0500
Dir.entries(dirpath)       # Updates directory atime.
File.atime(dirpath)        # => 2026-10-04 15:08:14.818746815 -0500 # Updated.
stat0.atime                # => 2026-10-04 15:07:26.194548647 -0500 # Not updated.
stat1 = File.stat(dirpath) # Take new shapshot.
stat1.atime                # => 2026-10-04 15:08:14.818746815 -0500 # Updated.
Dir.rmdir(dirpath)         # Clean up.
[ GitHub ]

  
# File 'file.c', line 1318

static VALUE
rb_stat_atime(VALUE self)
{
    return stat_time(statx_atimespec(get_stat(self)));
}

#birthtime ⇒ Time

Returns a new ::Time object containing the creation time of thesnapshot in self. See File System Timestamps:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
stat = File.stat(filepath) # Take shapshot.
stat.birthtime             # => 2026-10-05 13:14:24.844158315 -0500
File.delete(filepath)
stat.birthtime             # => 2026-10-05 13:14:24.844158315 -0500  # Unchanged.
[ GitHub ]

  
# File 'file.c', line 1417

static VALUE
rb_stat_birthtime(VALUE self)
{
    return statx_birthtime(get_stat(self));
}

#blksize ⇒ Integer?

Returns the block size of the filesystem of thesnapshot in self, or nil on Windows:

File.stat('/etc').blksize        # => 4096
File.stat('/etc/passwd').blksize # => 4096
File.stat('nosuch').blksize      # Raises Errno::ENOENT: No such file or directory.
[ GitHub ]

  
# File 'file.c', line 1121

static VALUE
rb_stat_blksize(VALUE self)
{
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
    return ULONG2NUM(get_stat(self)->ST_(blksize));
#else
    return Qnil;
#endif
}

#blocks ⇒ Integer?

Returns the number of filesystem blocks allocated for the entry in thesnapshot in self:

File.stat('/etc').blocks        # => 24
File.stat('/etc/passwd').blocks # => 8

Returns nil on Windows.

[ GitHub ]

  
# File 'file.c', line 1148

static VALUE
rb_stat_blocks(VALUE self)
{
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
# if SIZEOF_STRUCT_STAT_ST_BLOCKS > SIZEOF_LONG
    return ULL2NUM(get_stat(self)->ST_(blocks));
# else
    return ULONG2NUM(get_stat(self)->ST_(blocks));
# endif
#else
    return Qnil;
#endif
}

#ctime ⇒ Time

On Windows, returns the birthtime for the entry in self.

On other systems, returns the time of the most recent metadata change to thesnapshot in self:

dirpath = '/tmp/dir'
Dir.mkdir(dirpath)                     # Establishes directory ctime.
dirstat0 = File.stat(dirpath)          # Take directory snapshot.
dirstat0.ctime     # => 2026-10-06 11:07:18.073533542 -0500 # Initial directory ctime.
filepath = File.join(dirpath, 't.tmp') # => "/tmp/dir/t.tmp"
File.write(filepath, 'foo')            # Establishes file ctime; upates directory ctime.
filestat0 = File.stat(filepath)        # Take file snapshot.
filestat0.ctime    # => 2026-10-06 11:07:52.555027213 -0500 # Initial file ctime.
dirstat0.ctime     # => 2026-10-06 11:07:18.073533542 -0500 # Directory snapshot unchanged.
dirstat1 = File.stat(dirpath)          # Take new directory snapshot.
dirstat1.ctime     # => 2026-10-06 11:07:52.555027213 -0500 # Snapshot has new ctime.
File.write(filepath, 'bar')            # Updates file ctime.
filestat0.ctime    # => 2026-10-06 11:07:52.555027213 -0500 # Old file snapshot unchanged.
filestat1 = File.stat(filepath)        # Take new file snapshot.
filestat1.ctime    # => 2026-10-06 11:14:11.19290223 -0500  # Snapshot shows change.
FileUtils.rmtree(dirpath)              # Clean up.

See File System Timestamps.

[ GitHub ]

  
# File 'file.c', line 1389

static VALUE
rb_stat_ctime(VALUE self)
{
    return stat_time(statx_ctimespec(get_stat(self)));
}

#dev ⇒ Integer

Returns the device number for thesnapshot in self:

File.stat('/etc').dev # => 66306
File.stat('/dev').dev # => 5
[ GitHub ]

  
# File 'file.c', line 690

static VALUE
rb_stat_dev(VALUE self)
{
#if RUBY_USE_STATX
    unsigned int m = get_stat(self)->stx_dev_major;
    unsigned int n = get_stat(self)->stx_dev_minor;
    return ULL2NUM(makedev(m, n));
#elif SIZEOF_STRUCT_STAT_ST_DEV <= SIZEOF_DEV_T
    return DEVT2NUM(get_stat(self)->st_dev);
#elif SIZEOF_STRUCT_STAT_ST_DEV <= SIZEOF_LONG
    return ULONG2NUM(get_stat(self)->st_dev);
#else
    return ULL2NUM(get_stat(self)->st_dev);
#endif
}

#dev_major ⇒ Integer

Returns nil on Windows.

On other systems, returns the major part of the device number for thesnapshot in self:

File.stat('/etc').dev_major # => 259
File.stat('/dev').dev_major # => 0
[ GitHub ]

  
# File 'file.c', line 724

static VALUE
rb_stat_dev_major(VALUE self)
{
#if RUBY_USE_STATX
    return UINT2NUM(get_stat(self)->stx_dev_major);
#elif defined(major)
    return UINT2NUM(major(get_stat(self)->st_dev));
#else
    return Qnil;
#endif
}

#dev_minor ⇒ Integer

Returns nil on Windows.

On other systems, returns the minor part of the device number for thesnapshot in self:

File.stat('/etc').dev_minor # => 2
File.stat('/dev').dev_minor # => 5
[ GitHub ]

  
# File 'file.c', line 754

static VALUE
rb_stat_dev_minor(VALUE self)
{
#if RUBY_USE_STATX
    return UINT2NUM(get_stat(self)->stx_dev_minor);
#elif defined(minor)
    return UINT2NUM(minor(get_stat(self)->st_dev));
#else
    return Qnil;
#endif
}

#ftype ⇒ String

Returns the string type of the path in thesnapshot in self, one of:

  • 'file'.
  • 'directory'.
  • 'characterSpecial'.
  • 'blockSpecial'.
  • 'fifo'.
  • 'link'.
  • 'socket'.

Examples:

File.stat('/etc/passwd').ftype   # => "file"
File.stat('/etc').ftype          # => "directory"
File.stat('/dev/null').ftype     # => "characterSpecial"
File.stat('/dev/loop0').ftype    # => "blockSpecial"

File.mkfifo('/tmp/pipe', 0o666)
File.stat('/tmp/pipe').ftype     # => "fifo"

File.symlink('/etc/passwd', '/tmp/link')
File.lstat('/tmp/link').ftype    # => "link"

require 'socket'
UNIXServer.new('/tmp/socket')
File.stat('/tmp/socket').ftype   # => "socket"

File.delete('/tmp/link', '/tmp/pipe', '/tmp/socket') # Clean up.

Returns 'unknown' if the type cannot be determined.

[ GitHub ]

  
# File 'file.c', line 7184

static VALUE
rb_stat_ftype(VALUE obj)
{
    return rb_file_ftype(get_stat(obj)->ST_(mode));
}

#gid ⇒ Integer

On Windows, returns 0.

On other systems, returns the group identifier of the owner of the entry in thesnapshot in self:

File.stat('/etc').gid   # => 0
filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')
File.stat(filepath).gid # => 1000
File.delete(filepath)   # Clean up.
[ GitHub ]

  
# File 'file.c', line 910

static VALUE
rb_stat_gid(VALUE self)
{
    return GIDT2NUM(get_stat(self)->ST_(gid));
}

#initialize_copy(orig)

This method is for internal use only.
[ GitHub ]

  
# File 'file.c', line 7128

static VALUE
rb_stat_init_copy(VALUE copy, VALUE orig)
{
    if (!OBJ_INIT_COPY(copy, orig)) return copy;

    struct rb_stat *orig_rb_st;
    TypedData_Get_Struct(orig, struct rb_stat, &stat_data_type, orig_rb_st);

    struct rb_stat *copy_rb_st;
    TypedData_Get_Struct(copy, struct rb_stat, &stat_data_type, copy_rb_st);

    *copy_rb_st = *orig_rb_st;
    return copy;
}

#ino ⇒ Integer

Returns the inode value for the entry in thesnapshot in self:

File.stat('/etc').ino        # => 11141121
File.stat('/etc/passwd').ino # => 11141413
[ GitHub ]

  
# File 'file.c', line 782

static VALUE
rb_stat_ino(VALUE self)
{
    rb_io_stat_data *ptr = get_stat(self);
#ifdef HAVE_STRUCT_STAT_ST_INOHIGH
    /* assume INTEGER_PACK_LSWORD_FIRST and st_inohigh is just next of st_ino */
    return rb_integer_unpack(&ptr->st_ino, 2,
            SIZEOF_STRUCT_STAT_ST_INO, 0,
            INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_NATIVE_BYTE_ORDER|
            INTEGER_PACK_2COMP);
#else
    return UIANY2NUM(ptr->ST_(ino));
#endif
}

#inspect ⇒ String

Returns a string representation of the entry in thesnapshot in self; here we split the returned strings for readability:

puts File.stat('/etc/passwd').inspect.split(', ')

Output:

{#<}File::Stat dev=0x10302
ino=11141413
mode=0100644
nlink=1
uid=0
gid=0
rdev=0x0
size=3092
blksize=4096
blocks=8
atime=2026-10-07 11:25:01.307589569 -0500
mtime=2025-06-10 11:10:47.358999941 -0500
ctime=2025-06-10 11:10:47.38899994 -0500>
puts File.stat('/etc').inspect.split(', ')

Output:

{#<}File::Stat dev=0x10302
ino=11141121
mode=040755
nlink=143
uid=0
gid=0
rdev=0x0
size=12288
blksize=4096
blocks=24
atime=2026-10-04 14:29:46.907775905 -0500
mtime=2026-10-07 06:13:35.116915307 -0500
ctime=2026-10-07 06:13:35.116915307 -0500>
[ GitHub ]

  
# File 'file.c', line 1482

static VALUE
rb_stat_inspect(VALUE self)
{
    VALUE str;
    size_t i;
    static const struct {
        const char *name;
        VALUE (*func)(VALUE);
    } member[] = {
        {"dev",	    rb_stat_dev},
        {"ino",	    rb_stat_ino},
        {"mode",    rb_stat_mode},
        {"nlink",   rb_stat_nlink},
        {"uid",	    rb_stat_uid},
        {"gid",	    rb_stat_gid},
        {"rdev",    rb_stat_rdev},
        {"size",    rb_stat_size},
        {"blksize", rb_stat_blksize},
        {"blocks",  rb_stat_blocks},
        {"atime",   rb_stat_atime},
        {"mtime",   rb_stat_mtime},
        {"ctime",   rb_stat_ctime},
#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC)
        {"birthtime",   rb_stat_birthtime},
#endif
    };

    struct rb_stat* rb_st;
    TypedData_Get_Struct(self, struct rb_stat, &stat_data_type, rb_st);
    if (!rb_st->initialized) {
        return rb_sprintf("#<%s: uninitialized>", rb_obj_classname(self));
    }

    str = rb_str_buf_new2("#<");
    rb_str_buf_cat2(str, rb_obj_classname(self));
    rb_str_buf_cat2(str, " ");

    for (i = 0; i < sizeof(member)/sizeof(member[0]); i++) {
        VALUE v;

        if (i > 0) {
            rb_str_buf_cat2(str, ", ");
        }
        rb_str_buf_cat2(str, member[i].name);
        rb_str_buf_cat2(str, "=");
        v = (*member[i].func)(self);
        if (i == 2) {		/* mode */
            rb_str_catf(str, "0%lo", (unsigned long)NUM2ULONG(v));
        }
        else if (i == 0 || i == 6) { /* dev/rdev */
            rb_str_catf(str, "0x%"PRI_DEVT_PREFIX"x", NUM2DEVT(v));
        }
        else {
            rb_str_append(str, rb_inspect(v));
        }
    }
    rb_str_buf_cat2(str, ">");

    return str;
}

#mode ⇒ Integer

Returns an integer representing the permissions of the entry in thesnapshot in self; seeFilesystem Modes.

The integer is most usefully formatted as six octal digits:

'%06o' % File.stat('/etc').mode        # => "040755"
'%06o' % File.stat('/etc/passwd').mode # => "100644"
[ GitHub ]

  
# File 'file.c', line 816

static VALUE
rb_stat_mode(VALUE self)
{
    return UINT2NUM(ST2UINT(get_stat(self)->ST_(mode)));
}

#mtime ⇒ Time

Returns a new ::Time object containing themodification time of the object in thesnapshot in self:

filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')                                    # Create file.
File.mtime(filepath) # => 2026-10-13 10:48:10.477675435 -0500  # File mtime.
stat0 = File.stat(filepath)                                    # Take snapshot.
stat0.mtime          # => 2026-10-13 10:48:10.477675435 -0500  # Snapshot mtime.
File.write(filepath, 'bar')                                    # Modify file.
File.mtime(filepath) # => 2026-10-13 10:48:37.584264035 -0500  # File mtime updated.
stat0.mtime          # => 2026-10-13 10:48:10.477675435 -0500  # Snapshot mtime not updated.
stat1 = File.stat(filepath)                                    # Take new snapshot.
stat1.mtime          # => 2026-10-13 10:48:37.584264035 -0500  # New snapshot mtime.
File.delete(filepath)                                          # Clean up.
[ GitHub ]

  
# File 'file.c', line 1350

static VALUE
rb_stat_mtime(VALUE self)
{
    return stat_time(statx_mtimespec(get_stat(self)));
}

#rdev ⇒ Integer?

Returns the integer device type of the entry in thesnapshot in self.

On a non-Windows system, returns a filesystem-dependent device type; the returned value is non-zero only for character-special and block-special devices.

In these hexadecimal-formatted examples:

  • The two-low order digits contain the device-type minor value, which identifies a specific instance, port, or partition managed by a device driver; see #rdev_minor.
  • The higher-order digits contain the device-type major value, which identifies the device driver; see #rdev_major.
#### Character-special devices.
'%08x' % File.stat('/dev/null').rdev      # => "00000103"
'%08x' % File.stat('/dev/zero').rdev      # => "00000105"
'%08x' % File.stat('/dev/random').rdev    # => "00000108"
'%08x' % File.stat('/dev/tty').rdev       # => "00000500"
'%08x' % File.stat('/dev/tty0').rdev      # => "00000400"
'%08x' % File.stat('/dev/tty1').rdev      # => "00000401"
#### Block-special devices.
'%08x' % File.stat('/dev/nvme0n1').rdev   # => "00010300"
'%08x' % File.stat('/dev/nvme0n1p1').rdev # => "00010301"
'%08x' % File.stat('/dev/nvme0n1p2').rdev # => "00010302"

On Windows, returns a number corresponding to the given device:

File.stat('C:/').rdev      # => 2
File.stat('C:/Users').rdev # => 2
File.stat('D:/').rdev      # => 3
[ GitHub ]

  
# File 'file.c', line 962

static VALUE
rb_stat_rdev(VALUE self)
{
#if RUBY_USE_STATX
    unsigned int m = get_stat(self)->stx_rdev_major;
    unsigned int n = get_stat(self)->stx_rdev_minor;
    return ULL2NUM(makedev(m, n));
#elif !defined(HAVE_STRUCT_STAT_ST_RDEV)
    return Qnil;
#elif SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_DEV_T
    return DEVT2NUM(get_stat(self)->ST_(rdev));
#elif SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_LONG
    return ULONG2NUM(get_stat(self)->ST_(rdev));
#else
    return ULL2NUM(get_stat(self)->ST_(rdev));
#endif
}

#rdev_major ⇒ Integer

Returns the integer major portion of the device type of the entry in thesnapshot in self; see also #rdev, which returns the entire device type (major and minor).

On a non-Windows system, returns a filesystem-dependent major device type, which identifies the device driver; the returned value is non-zero only for character-special and block-special devices:

#### Character-special devices.
'%04x' % File.stat('/dev/null').rdev_major      # => "0001"
'%04x' % File.stat('/dev/zero').rdev_major      # => "0001"
'%04x' % File.stat('/dev/random').rdev_major    # => "0001"
'%04x' % File.stat('/dev/tty').rdev_major       # => "0005"
'%04x' % File.stat('/dev/tty0').rdev_major      # => "0004"
'%04x' % File.stat('/dev/tty1').rdev_major      # => "0004"
#### Block-special devices.
'%04x' % File.stat('/dev/nvme0n1').rdev_major   # => "0103"
'%04x' % File.stat('/dev/nvme0n1p1').rdev_major # => "0103"
'%04x' % File.stat('/dev/nvme0n1p2').rdev_major # => "0103"

On Windows, returns nil.

[ GitHub ]

  
# File 'file.c', line 1012

static VALUE
rb_stat_rdev_major(VALUE self)
{
#if RUBY_USE_STATX
    return UINT2NUM(get_stat(self)->stx_rdev_major);
#elif defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(major)
    return UINT2NUM(major(get_stat(self)->ST_(rdev)));
#else
    return Qnil;
#endif
}

#rdev_minor ⇒ Integer

Returns the integer minor portion of the device type of the entry in thesnapshot in self; see also #rdev, which returns the entire device type (major and minor).

On a non-Windows system, returns a filesystem-dependent minor device type, which identifies a specific instance, port, or partition on its device; the returned value is non-zero only for character-special and block-special devices:

#### Character-special devices.
'%02x' % File.stat('/dev/null').rdev_minor      # => "03"
'%02x' % File.stat('/dev/zero').rdev_minor      # => "05"
'%02x' % File.stat('/dev/random').rdev_minor    # => "08"
'%02x' % File.stat('/dev/tty').rdev_minor       # => "00"
'%02x' % File.stat('/dev/tty0').rdev_minor      # => "00"
'%02x' % File.stat('/dev/tty1').rdev_minor      # => "01"
#### Block-special devices.
'%02x' % File.stat('/dev/nvme0n1').rdev_minor   # => "00"
'%02x' % File.stat('/dev/nvme0n1p1').rdev_minor # => "01"
'%02x' % File.stat('/dev/nvme0n1p2').rdev_minor # => "02"

On Windows, returns nil.

[ GitHub ]

  
# File 'file.c', line 1056

static VALUE
rb_stat_rdev_minor(VALUE self)
{
#if RUBY_USE_STATX
    return UINT2NUM(get_stat(self)->stx_rdev_minor);
#elif defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(minor)
    return UINT2NUM(minor(get_stat(self)->ST_(rdev)));
#else
    return Qnil;
#endif
}

#uid ⇒ Integer

Returns the integer id of the owner of the entry in thesnapshot in self:

Process.uid # => 0                # Superuser; can change owner.
filepath = '/tmp/t.tmp'
File.write(filepath, 'foo')       # Create file.
stat0 = File.stat(filepath)       # Take snapshot.
stat0.uid   # => 0                # Snapshot has uid.
File.chown(1000, 1000, filepath)  # Change owner.
stat0.uid   # => 0                # Snapshot not updated.
stat1 = File.stat(filepath)       # Take new snapshot.
stat1.uid   # => 1000             # Snapshot has new uid.
File.delete(filepath)             # Clean up.
[ GitHub ]

  
# File 'file.c', line 883

static VALUE
rb_stat_uid(VALUE self)
{
    return UIDT2NUM(get_stat(self)->ST_(uid));
}