IO, Files and Directories
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Every Ruby program talks to the outside world through file descriptors: standard input and output, files, directories, pipes and
sockets. Ruby wraps them in a small family of classes: IO is the base class with the reading, writing and buffering behavior,
File adds the file system (names, permissions, locking), Dir lists and creates directories, Pathname is an object-oriented
view of a path, FileUtils is the shell-like toolbox (mkdir -p, cp -r, rm -rf), and Tempfile and StringIO give you
scratch files and in-memory streams that behave like files. This page covers all of them: opening and closing files safely,
modes and flags, encodings and newline conversion, reading and writing whole files, lines, bytes and chunks, paths and metadata,
links, globbing, Pathname (a core class from Ruby 4.0), FileUtils, temporary files, StringIO, IO::Buffer, pipes,
non-blocking reads, locking, atomic replacement, error handling and path safety.
Neighboring topics live elsewhere: how strings carry encodings is in
Encodings and Unicode, spawning programs and the removal of
Kernel#open("|cmd") in 4.0 are in Processes, Signals and Shell,
sockets and HTTP are in Networking and HTTP, reading and writing JSON,
YAML, CSV and Marshal is in
Serialization: JSON, YAML, CSV and Marshal,
the Errno exception classes are placed in the hierarchy by
Exceptions and Error Handling, and the safety side of file
names and temporary files is revisited in Security. For the same ideas in other
languages see Go, Rust and
Python.
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop using
the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings), apart from one deliberate exception that is
marked where it occurs. The output shown is the real output. Examples that create files work inside a directory made by
Dir.mktmpdir, which is removed when its block ends, so they leave nothing behind; temporary directory names are abbreviated in the
output, and examples were run with a UTF-8 locale. Each example is self-contained; the ones that use the shelf scenario say so.
The I/O classes at a glance
| Class or module | What it is for | Available |
|---|---|---|
|
Streams over file descriptors: |
Core. |
|
Named files: |
Core. |
|
File metadata objects, the flag constants ( |
Core. |
|
Listing, globbing, creating and removing directories, the working directory. |
Core. |
|
A path as an object with |
Core from Ruby 4.0 (a default gem that needed |
|
Shell-like operations: |
|
|
Temporary files that remove themselves. |
|
|
An in-memory object with the |
|
|
Depth-first directory walk with pruning. |
|
|
A block of memory (or a mapped file) addressed by offset and typed values; the basis of the fiber scheduler’s |
Core, still reported as experimental. |
|
The program’s input files, its own trailing data, and the standard streams. |
Core. |
All the file system functions raise a subclass of SystemCallError (the Errno family) when the operating system reports a
failure, so a missing file is Errno::ENOENT and a permission problem is Errno::EACCES; the section on errors lists them.
Kernel#open and the old IO.popen shortcuts are not covered here: prefer File.open for files, and see
Processes, Signals and Shell for processes.
Opening and closing files
File.open with a block
A file descriptor is a limited operating-system resource, and a buffered stream can hold unwritten data, so every file that is
opened must be closed. The idiom that makes this automatic is the block form: File.open(path, mode) { |file| ... } opens the
file, yields it, closes it when the block ends (normally, through break or return, or through an exception) and returns the
value of the block. Without a block, File.open and File.new return an open file and you must call close yourself, normally
inside an ensure. Using a closed stream raises IOError with the message closed stream.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "notes.txt")
# The block form closes the file when the block ends, even on an exception.
result = File.open(path, "w") do |file|
file.puts "first line"
file.class
end
puts "block value: #{result}"
leaked = nil
begin
File.open(path) do |file|
leaked = file
raise "boom"
end
rescue RuntimeError => e
puts "rescued #{e.message}, closed? #{leaked.closed?}"
end
# Without a block you own the file and must close it.
file = File.new(path)
puts file.read
file.close
puts "closed? #{file.closed?}"
begin
file.read
rescue IOError => e
puts "#{e.class}: #{e.message}"
end
# IO#path (3.2) returns the path the file was opened with.
puts File.open(path, &:path) == path
end
Output:
block value: File
rescued boom, closed? true
first line
closed? true
IOError: closed stream
true
Source: Ruby 4.0 documentation: File and IO
The one-shot class methods File.read, File.write, File.readlines, File.foreach and their binread and binwrite
variants open and close the file for you, so use them whenever the whole operation is a single call. Reach for the block form of
File.open when you need several operations on one handle or a specific mode. IO#path (Ruby 3.2) returns the path a file was opened
with, and File.new and File.open accept the same arguments; File.open with a block is the one that closes for you.
A file that is garbage collected without being closed is closed by a finalizer, but you cannot predict when, and written data
is only guaranteed to be flushed by close or flush. Do not depend on it.
Modes and flags
The second argument of File.open is a mode string, an integer made from File::Constants flags, or absent (read-only). The
keyword form mode:, flags: and perm: is equivalent and often clearer.
| Mode | Meaning |
|---|---|
|
Read only, starting at the beginning. The file must exist ( |
|
Read and write, starting at the beginning, without truncating. The file must exist. |
|
Write only. Creates the file or truncates it to zero length. |
|
Read and write. Creates or truncates. |
|
Write only, every write at the end of the file (append). Creates the file if needed. |
|
Read and append. Reads start at the beginning, writes go to the end. |
|
Binary mode ( |
|
Text mode ( |
|
With |
|
An external (and optionally internal) encoding, |
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "log.txt")
File.write(path, "one\n")
File.open(path, "a") { |f| f.puts "two" }
puts File.read(path).inspect # => "one\ntwo\n"
File.open(path, "r+") do |f|
f.write("ONE")
f.rewind
puts f.read.inspect
end
File.open(path, "w") { |f| f.print "truncated" }
puts File.size(path)
# "x" adds File::EXCL: fail if the file exists.
begin
File.open(path, "wx") { |f| f.puts "never" }
rescue Errno::EEXIST => e
puts "#{e.class}: #{e.message.sub(dir, '<dir>')}"
end
end
Output:
"one\ntwo\n"
"ONE\ntwo\n"
9
Errno::EEXIST: File exists @ rb_sysopen - <dir>/log.txt
Source: Ruby 4.0 documentation: File and File::Constants
The integer form is the one that exposes everything the operating system offers. The flags are the constants in
File::Constants (also reachable as File::RDONLY and so on): RDONLY, WRONLY, RDWR, APPEND, CREAT, EXCL, TRUNC,
NONBLOCK, NOCTTY, SYNC, DSYNC, NOFOLLOW, BINARY and SHARE_DELETE. Combine them with |. The third argument is the
permission mask applied when the file is created (before the process umask), in octal. A new file that holds anything sensitive
should be created with a restrictive mask such as 0o600.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
# Integer flags and a permission argument.
flags = File::WRONLY | File::CREAT | File::TRUNC
path = File.join(dir, "private.txt")
File.open(path, flags, 0o600) { |f| f.puts "first" }
puts format("%o", File.stat(path).mode & 0o777)
# Keyword form of mode:, flags: and perm:.
File.open(path, mode: "a", perm: 0o600) { |f| f.puts "more" }
puts File.readlines(path, chomp: true).inspect
begin
File.open(File.join(dir, "missing.txt"))
rescue Errno::ENOENT => e
puts "#{e.class}: #{e.message.sub(dir, '<dir>')}"
puts e.errno == Errno::ENOENT::Errno
end
end
Output:
600
["first", "more"]
Errno::ENOENT: No such file or directory @ rb_sysopen - <dir>/missing.txt
true
Source: File::Constants and SystemCallError
File::NULL is the platform’s null device (/dev/null, or NUL on Windows), handy as a sink. IO#sync = true turns off Ruby’s
write buffering for that stream, IO#flush pushes buffered data to the operating system, and IO#fsync and IO#fdatasync ask
the operating system to push it to the disk; they are the difference between "written" and "durable".
Kernel#open and other shortcuts
Kernel#open("path") opens a file like File.open, but until Ruby 3.3 a path starting with | ran a shell command instead
(deprecated in 3.3 together with the same behavior of IO.read, IO.readlines, IO.foreach, IO.write, IO.binread and
URI.open). In Ruby 4.0 that behavior is removed: all of these now treat the string as a file name, and a name such as
"|echo hi" raises Errno::ENOENT. Use File.open (or File.read) for files and Open3 or IO.popen with an argument array
for programs; the processes page covers the command-injection reasons.
Encodings and newlines
A file holds bytes; Ruby turns them into strings with an encoding. Every IO has an external encoding (how the bytes in the
file are interpreted) and an optional internal encoding (what the strings you get should be converted to). The external
encoding defaults to Encoding.default_external, which comes from the locale (LANG, LC_ALL) and is UTF-8 on a typical modern
system; with no internal encoding set, strings are tagged with the external encoding and no conversion happens. Setting both,
as in "r:iso-8859-1:utf-8", makes Ruby transcode while reading. Writing converts from the string’s encoding to the external
encoding when they differ. The command-line flags -E ext:int and -U change the defaults for the whole process.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "latin1.txt")
File.binwrite(path, "caf\xE9\n".b)
File.open(path) { |f| puts f.read.encoding } # default external, UTF-8
File.open(path, "r:iso-8859-1") { |f| puts f.read.encoding }
# external:internal transcodes while reading.
File.open(path, "r:iso-8859-1:utf-8") do |f|
text = f.read
puts "#{text.inspect} #{text.encoding}"
end
# Keyword form.
text = File.read(path, encoding: "iso-8859-1:utf-8")
puts text.valid_encoding?
# Writing transcodes from the string encoding to the external one.
out = File.join(dir, "out.txt")
File.write(out, "café", encoding: "iso-8859-1")
p File.binread(out) # => "caf\xE9"
# Byte order marks: "bom|utf-8" strips a UTF-8 BOM.
bom = File.join(dir, "bom.txt")
File.binwrite(bom, "\xEF\xBB\xBFhello".b)
p File.read(bom).start_with?("\uFEFF")
p File.read(bom, encoding: "bom|utf-8")
# Binary mode: no newline conversion, ASCII-8BIT strings.
File.open(path, "rb") { |f| puts f.read.encoding }
puts Encoding.default_external
end
Output:
UTF-8
ISO-8859-1
"café\n" UTF-8
true
"caf\xE9"
true
"hello"
ASCII-8BIT
UTF-8
Source: Ruby 4.0 documentation: IO and Encodings
Things to remember:
-
A file with a byte order mark needs
"bom|utf-8": without it the BOM stays in the string as""and breaks the first comparison or CSV header.IO#set_encoding_by_bomdoes the same on an already-open binary stream. -
Binary mode (
"rb",File.binread,IO#binmode) gives ASCII-8BIT (Encoding::BINARY) strings and disables newline and encoding conversion. Use it for images, archives,Marshaldata and anything you pass topackandunpack. -
Reading invalid bytes does not raise: the string just reports
valid_encoding?asfalse, and the error surfaces later, for example in a regular expression match. Transcoding with"r:ext:int"does raiseEncoding::InvalidByteSequenceErrororEncoding::UndefinedConversionError, unless you passinvalid: :replaceandundef: :replace(withreplace:). -
Encoding.default_internalisnilunless you set it; setting it globally is discouraged because every library then gets transcoded text.
Newline conversion
On Unix a file is read as is: a CRLF line ending keeps its \r. Text mode ("rt") or the universal_newline: true option
converts CRLF and CR to LF on read, and crlf_newline: true or cr_newline: true converts LF on write (newline: :universal,
:crlf, :cr and :lf are the keyword spellings). chomp: true on the line-reading methods strips either ending. On Windows, text mode
(the default there) converts automatically, which is why files written on one system sometimes carry stray \r on the other.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "crlf.txt")
File.binwrite(path, "a\r\nb\r\n")
p File.read(path) # binary-safe default on Unix
p File.read(path, mode: "rt") # text mode, universal newlines
p File.open(path, universal_newline: true, &:read)
p File.readlines(path, chomp: true) # chomp strips CRLF too
out = File.join(dir, "out.txt")
File.open(out, "w", crlf_newline: true) { |f| f.puts "x" }
p File.binread(out)
File.open(path) do |f|
f.timeout = 2.5 # IO#timeout raises IO::TimeoutError on blocking reads
puts f.timeout
f.sync = true
f.fsync
f.fdatasync
end
puts IO::TimeoutError.ancestors.first(3).inspect
end
Output:
"a\r\nb\r\n"
"a\nb\n"
"a\nb\n"
["a", "b"]
"x\r\n"
2.5
[IO::TimeoutError, IOError, StandardError]
Source: Ruby 4.0 documentation: IO
The last lines of that example show IO#timeout (Ruby 3.2): a stream can be given a timeout in seconds after which a blocking
read or write raises IO::TimeoutError (a subclass of IOError) instead of waiting forever. sync, fsync and fdatasync
are described above.
Reading and writing
Whole-file helpers
| Method | What it does |
|---|---|
|
The whole file, or |
|
The same in binary mode (ASCII-8BIT). |
|
Creates or truncates the file, writes, closes, and returns the number of bytes written. Accepts |
|
Binary-mode write. |
|
An array of lines. Loads the whole file: avoid for large files. |
|
Yields one line at a time (an enumerator without a block); memory use stays flat. |
|
The same methods on |
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "books.txt")
bytes = File.write(path, "Dune\nEmma\nUlysses\n")
puts "wrote #{bytes} bytes"
puts File.read(path, 4).inspect # first four bytes
puts File.read(path, 4, 5).inspect # four bytes from offset 5
p File.readlines(path)
p File.readlines(path, chomp: true)
File.foreach(path, chomp: true).with_index(1) do |line, number|
puts "#{number}: #{line}"
end
end
Output:
wrote 18 bytes
"Dune"
"Emma"
["Dune\n", "Emma\n", "Ulysses\n"]
["Dune", "Emma", "Ulysses"]
1: Dune
2: Emma
3: Ulysses
Source: Ruby 4.0 documentation: File and IO
File.read(path) loads the whole file into one string, which is simple and fast for files up to a few megabytes and wrong for a
log file of several gigabytes. For big inputs iterate with File.foreach, IO#each_line or IO#read(size) in a loop. For
copying use IO.copy_stream (below), which does not build the file in memory. File.write replaces the file in place: if your
program is killed in the middle, the file is truncated or partial; the atomic replacement pattern later in this page avoids that.
The cursor: gets, readline, each_line, seek
An open file has a position. gets returns the next line (including its newline) or nil at the end of the file; readline does the same
but raises EOFError at the end, which suits a protocol where the end is unexpected. read without an argument reads to the end and
returns "" at the end of the file, while read(n) returns nil at the end. eof? tests the position, pos, seek, rewind
and lineno move and report it, IO::SEEK_SET, IO::SEEK_CUR and IO::SEEK_END select the reference point (seek also accepts
:SET, :CUR and :END symbols). each_line, each_char, each_byte, readlines and readchar and readbyte read from the cursor, and the
line methods accept a separator ("\n" by default; nil means "everything"; "" means "paragraph mode", split on blank lines), a byte
limit and chomp: true.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "books.txt")
File.write(path, "Dune\nEmma\nUlysses\n")
File.open(path) do |file|
puts file.gets.inspect
puts file.lineno
puts file.readline(chomp: true)
puts file.pos
file.each_line { |line| puts "rest: #{line.chomp}" }
puts file.eof?
puts file.gets.inspect # nil at end of file
begin
file.readline
rescue EOFError => e
puts "#{e.class}: #{e.message}"
end
file.rewind
file.seek(-8, IO::SEEK_END)
puts file.read.inspect
end
end
Output:
"Dune\n"
1
Emma
10
rest: Ulysses
true
nil
EOFError: end of file reached
"Ulysses\n"
Source: Ruby 4.0 documentation: IO
Reading in a loop of read(size) until eof? or read returns nil is the pattern for binary data. IO#readpartial(maxlen)
returns whatever is available (at least one byte) and raises EOFError at the end: it is the right call for pipes and sockets,
where read(n) would wait for all n bytes. IO#pread(length, offset) and IO#pwrite(string, offset) read and write at an
offset without moving the position, which avoids races on the shared position when several threads read one file.
Writing
puts writes its arguments each followed by a newline (and flattens arrays), print writes without adding anything, write writes
any number of strings and returns the byte count, << writes one object and returns the stream so calls chain, and printf formats.
Output is buffered by Ruby; close flushes, and so does flush or sync = true. In "a" mode every write goes to the end even
if you seek elsewhere.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "books.txt")
File.write(path, "Dune\nEmma\nUlysses\n")
File.open(path, "a") do |file|
file << "Hamlet" << "\n"
file.printf("%-8s|\n", "pad")
file.write("a", "b", "\n")
end
puts File.read(path)
# Reading in chunks keeps memory use flat.
chunks = []
File.open(path) { |f| chunks << f.read(10) until f.eof? }
p chunks.map(&:size)
# Splitting by a separator or limit.
File.open(path) { |f| p f.gets("a") }
File.open(path) { |f| p f.gets(nil, 6) }
File.open(path) { |f| p f.each_char.first(3) }
File.open(path) { |f| p f.each_byte.first(3) }
end
Output:
Dune
Emma
Ulysses
Hamlet
pad |
ab
[10, 10, 10, 8]
"Dune\nEmma"
"Dune\nE"
["D", "u", "n"]
[68, 117, 110]
Source: Ruby 4.0 documentation: IO
The $stdout and $stderr objects are ordinary IO instances (puts and warn are shortcuts to them), so everything above
applies, and $stdout = StringIO.new redirects output for tests. $stdout.sync is false by default: output is line-buffered on a terminal but block-buffered when piped, which is why
output sent to another program can appear late or interleave with $stderr. Set $stdout.sync = true for logs that must show up immediately.
Copying and binary data
IO.copy_stream(src, dst, copy_length = nil, src_offset = nil) copies between file names, IO objects or any object with
readpartial and write, and returns the number of bytes copied. It uses the operating system’s fast copy when it can and does
not allocate a string for the whole content.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
src = File.join(dir, "src.bin")
dst = File.join(dir, "dst.bin")
File.binwrite(src, [1, 2, 3, 255].pack("C*"))
p File.binread(src).bytes
copied = IO.copy_stream(src, dst)
puts "copied #{copied} bytes"
puts File.binread(dst) == File.binread(src)
File.open(src, "rb") do |f|
puts f.read.encoding
puts f.binmode?
end
# pread/pwrite do not move the file position.
File.open(src, "rb") do |f|
p f.pread(2, 1).bytes
puts f.pos
end
# StringIO source and destination work with copy_stream too.
require "stringio"
sink = StringIO.new
IO.copy_stream(StringIO.new("hello"), sink)
puts sink.string
end
Output:
[1, 2, 3, 255]
copied 4 bytes
true
ASCII-8BIT
true
[2, 3]
0
hello
Source: Ruby 4.0 documentation: IO
Standard streams, ARGF and DATA
$stdin, $stdout and $stderr are file descriptors 0, 1 and 2 (the constants STDIN, STDOUT and STDERR hold the original objects, so
assigning to $stdout does not change STDOUT). Kernel#gets, readline and readlines do not read $stdin: they read ARGF,
the concatenation of the files named in ARGV, or standard input when ARGV is empty. That is how ruby script.rb a.txt b.txt can read two files
as one stream, and why gets in a script that receives arguments tries to open them as file names (and raises Errno::ENOENT
if they are not). Use $stdin.gets when you mean standard input. DATA is a File positioned just after a line containing only __END__
in the main script, so a script can carry its own data.
# frozen_string_literal: true
$stdout.puts "to stdout"
warn "to stderr" # Kernel#warn writes to $stderr
$stdout.sync = true
puts $stdout.sync, $stdout.fileno, $stderr.fileno, $stdin.fileno
puts $stdout.tty?.class
puts $PROGRAM_NAME == __FILE__
puts DATA.class
DATA.each_line { |line| puts "DATA: #{line}" }
__END__
inline one
inline two
Output (run with standard error discarded; tty? is false because the output is piped):
to stdout
true
1
2
0
FalseClass
true
File
DATA: inline one
DATA: inline two
Source: Ruby 4.0 documentation: ARGF and IO
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
one = File.join(dir, "one.txt")
two = File.join(dir, "two.txt")
File.write(one, "Dune\nEmma\n")
File.write(two, "Ulysses\n")
# ARGF reads the files named in ARGV one after another (or stdin if empty).
ARGV.replace([one, two])
ARGF.each_line do |line|
puts "#{File.basename(ARGF.filename)}:#{ARGF.file.lineno}: #{line}"
end
puts ARGF.class
# Paragraph mode: an empty separator splits on blank lines.
text = File.join(dir, "para.txt")
File.write(text, "a\nb\n\n\nc\n")
p File.foreach(text, "").to_a
p File.foreach(text, "", chomp: true).to_a
p File.foreach(text, 2).to_a
end
Output:
one.txt:1: Dune
one.txt:2: Emma
two.txt:1: Ulysses
ARGF.class
["a\nb\n\n", "c\n"]
["a\nb", "c\n"]
["a\n", "b\n", "\n", "\n", "c\n"]
Paragraph mode ("" as the separator) splits on one or more blank lines and ends each paragraph with a single "\n\n". With chomp: true the paragraph
break is removed from the paragraphs that have one, while the last paragraph keeps its own final newline, a subtlety worth testing before relying on it. A numeric argument limits the bytes per line, so a long line arrives in pieces.
Source: Ruby 4.0 documentation: ARGF and File
Paths and file metadata
Working with names
The path functions on File operate on strings and never touch the disk (except realpath). File.join assembles
with the platform separator and collapses duplicate separators, basename and dirname split, extname returns the last extension
(with its dot), File.dirname(path, level) strips several components (Ruby 3.1), expand_path resolves .., . and "~" against a base
(the current directory by default) and absolute_path does the same without expanding "~". realpath resolves symbolic links and raises if the file is missing.
File.fnmatch is the glob matcher behind Dir.glob; by default * does not match a leading dot, and File::FNM_DOTMATCH,
FNM_PATHNAME, FNM_EXTGLOB, FNM_CASEFOLD and FNM_NOESCAPE change that.
# frozen_string_literal: true
path = "/var/lib/shelf/data/books.2026.csv"
puts File.basename(path) # => "books.2026.csv"
puts File.basename(path, ".csv") # => "books.2026"
puts File.basename(path, ".*") # => "books.2026"
puts File.dirname(path) # => "/var/lib/shelf/data"
puts File.dirname(path, 2) # => "/var/lib/shelf"
puts File.extname(path) # => ".csv"
p File.split(path)
puts File.join("data", "books", "a.csv")
puts File.join("data/", "/books") # duplicate separators are collapsed
puts File.expand_path("../x.txt", "/a/b")
puts File.expand_path("~") == Dir.home
puts File.absolute_path("x", "/a/b")
puts File.absolute_path?("/a")
puts File.fnmatch("*.csv", "books.csv")
puts File.fnmatch("*.csv", ".hidden.csv")
puts File.fnmatch("*.csv", ".hidden.csv", File::FNM_DOTMATCH)
puts File.fnmatch("**/*.rb", "lib/shelf/book.rb", File::FNM_PATHNAME)
puts File::SEPARATOR
Output:
books.2026.csv
books.2026
books.2026
/var/lib/shelf/data
/var/lib/shelf
.csv
["/var/lib/shelf/data", "books.2026.csv"]
data/books/a.csv
data/books
/a/x.txt
true
/a/b/x
true
true
false
true
true
/
Source: Ruby 4.0 documentation: File
Predicates, stat and permissions
| Method | Answers |
|---|---|
|
Does anything exist at the path (it follows symbolic links)? Also |
|
The kind of entry; |
|
Permission for the effective user. |
|
Size in bytes; empty means size zero. |
|
Time of modification, access, inode change, and creation (where the system records it). Return |
|
A |
|
Change permission bits, ownership and timestamps. |
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "a.txt")
puts File.exist?(path)
File.write(path, "hello")
puts File.exist?(path), File.file?(path), File.directory?(dir)
puts File.size(path), File.empty?(path)
puts File.readable?(path), File.writable?(path), File.executable?(path)
puts File.mtime(path).class
puts File.ftype(path), File.ftype(dir)
stat = File.stat(path)
puts stat.size, stat.file?, format("%o", stat.mode & 0o777)
File.chmod(0o600, path)
puts format("%o", File.stat(path).mode & 0o777)
File.utime(Time.at(0), Time.at(86_400), path)
puts File.atime(path).utc, File.mtime(path).utc
puts File.respond_to?(:exists?)
begin
File.method(:exists?)
rescue NameError => e
puts e.message
end
end
Output:
false
true
true
true
5
false
true
true
false
Time
file
directory
5
true
644
600
1970-01-01 00:00:00 UTC
1970-01-02 00:00:00 UTC
false
undefined method 'exists?' for class '#<Class:File>'
Source: Ruby 4.0 documentation: File and File::Stat
File.exists? and Dir.exists? were deprecated for years and are removed in Ruby 3.2: calling them raises NoMethodError. The fix
is the singular exist?. RuboCop’s Lint/DeprecatedClassMethods flags the old spelling, which is why the example above probes it
with File.method(:exists?), and code written before 3.2 still contains them.
A check such as File.exist?(path) followed by an action on the path is a race: another process can create or delete the
file between the two calls (a time-of-check to time-of-use problem). Prefer the single operation that fails if the state is
wrong: File.open(path, "wx") to create exclusively, FileUtils.mkdir_p (which is idempotent), or rescue Errno::ENOENT around the read.
RuboCop’s Lint/NonAtomicFileOperation warns about the check-then-act pattern.
Renaming, deleting and links
File.rename(old, new) renames or moves within one file system and replaces an existing destination atomically on POSIX systems;
across file systems it raises Errno::EXDEV (FileUtils.mv falls back to copy and delete). File.delete (alias unlink) removes
files and raises Errno::ENOENT if the file is gone. File.symlink(target, link) makes a symbolic link (the target is stored as written, it does not
need to exist), File.link a hard link (a second name for the same inode), File.readlink reads the stored target,
File.identical? tests whether two names refer to the same file, and File.truncate shortens or extends a file.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
a = File.join(dir, "a.txt")
b = File.join(dir, "b.txt")
link = File.join(dir, "link.txt")
File.write(a, "data")
File.rename(a, b)
File.symlink(b, link)
puts File.symlink?(link), File.readlink(link) == b
puts File.lstat(link).symlink?, File.stat(link).symlink?
puts File.realpath(link) == File.realpath(b)
File.delete(link)
puts File.exist?(link)
puts File.exist?(b)
begin
File.delete(link)
rescue SystemCallError => e
puts e.class
end
File.link(b, a) # hard link
puts File.stat(a).nlink
puts File.identical?(a, b)
File.truncate(b, 2)
puts File.read(a)
end
Output:
true
true
true
false
true
false
true
Errno::ENOENT
2
true
da
Source: Ruby 4.0 documentation: File
Directories
Listing
Dir.children(dir) returns the entry names except . and .., Dir.entries(dir) includes them, and Dir.each_child(dir) and
Dir.foreach(dir) iterate without building an array. The order is whatever the file system returns: it is neither alphabetical nor stable,
so sort when the order matters. Names are bare names, not paths; join them with the directory. Dir.empty?(dir) tests for an empty
directory, and Dir.exist? for existence. Dir.mkdir(path, mode) creates one level (the parent must exist), Dir.rmdir removes an empty
directory (Errno::ENOTEMPTY otherwise), and FileUtils.mkdir_p and rm_rf handle trees.
Dir.glob
Dir.glob(pattern, flags = 0, base: nil, sort: true) (also Dir[]) expands shell-style patterns and returns an array of paths:
| Pattern | Matches |
|---|---|
|
Any run of characters within one path segment; does not match a leading dot. |
|
Zero or more directories, recursively: |
|
Exactly one character. |
|
One character from the set, the range, or not in the set. |
|
Alternatives; the braces expand before matching, so |
a backslash |
Escapes a metacharacter (inside a double-quoted Ruby string the backslash itself must be doubled). |
The flags are the File::FNM_* constants; File::FNM_DOTMATCH includes hidden files. base: (Ruby 2.5) makes the search relative to a
directory and returns paths relative to it, which avoids building absolute patterns (a directory name that contains * or [ is
then not interpreted as a pattern). Since Ruby 3.0, results are sorted by default, and sort: false returns them in file
system order, which can be faster on huge trees. An array of patterns is accepted. Glob patterns always use /, even on Windows.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
Dir.mkdir(File.join(dir, "lib"))
Dir.mkdir(File.join(dir, "lib", "shelf"))
%w[lib/a.rb lib/shelf/b.rb lib/shelf/c.txt .hidden README.md].each do |name|
File.write(File.join(dir, name), "")
end
p Dir.children(dir).sort
p Dir.entries(dir).sort
p Dir.each_child(dir).to_a.sort
p Dir.glob("**/*.rb", base: dir)
p Dir.glob("*", base: dir)
p Dir.glob("*", File::FNM_DOTMATCH, base: dir)
p Dir.glob("lib/**/*.{rb,txt}", base: dir)
p Dir.glob(%w[*.md lib/*.rb], base: dir)
p Dir.glob("lib/shelf/?.rb", base: dir)
p Dir.glob("lib/[ab]*", base: dir)
p Dir[File.join(dir, "lib", "*")].map { |f| File.basename(f) }.sort
puts Dir.exist?(dir), Dir.empty?(File.join(dir, "lib"))
p Dir.glob("lib/*", base: dir, sort: false).sort
end
Output:
[".hidden", "README.md", "lib"]
[".", "..", ".hidden", "README.md", "lib"]
[".hidden", "README.md", "lib"]
["lib/a.rb", "lib/shelf/b.rb"]
["README.md", "lib"]
[".", ".hidden", "README.md", "lib"]
["lib/a.rb", "lib/shelf/b.rb", "lib/shelf/c.txt"]
["README.md", "lib/a.rb"]
["lib/shelf/b.rb"]
["lib/a.rb"]
["a.rb", "shelf"]
true
false
["lib/a.rb", "lib/shelf"]
Source: Ruby 4.0 documentation: Dir
The working directory
Dir.pwd is the process-wide current directory and Dir.chdir(path) changes it. With a block the directory is restored afterwards.
The directory belongs to the whole process, not to the thread, so Dir.chdir in a block while another thread also uses Dir.chdir
raises RuntimeError (conflicting chdir during another chdir block), and relative paths used by a different thread silently change
meaning. In libraries pass base: or build absolute paths instead of changing directory. Dir.home returns the home directory and
Dir.open returns a Dir object that offers children, each_child, read and close.
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
here = Dir.pwd
Dir.chdir(dir) do
puts Dir.pwd == File.realpath(dir)
File.write("rel.txt", "relative")
end
puts Dir.pwd == here
puts File.exist?(File.join(dir, "rel.txt"))
begin
Dir.rmdir(dir)
rescue SystemCallError => e
puts e.class
end
begin
Dir.mkdir(dir)
rescue SystemCallError => e
puts e.class
end
Dir.open(dir) do |d|
puts d.class, d.path == dir
p d.children
end
puts Dir.tmpdir.class
end
Output:
true
true
true
Errno::ENOTEMPTY
Errno::EEXIST
Dir
true
["rel.txt"]
String
Source: Ruby 4.0 documentation: Dir
Dir.fchdir and Dir.for_fd (Ruby 3.3) work with directory file descriptors on systems that support them.
Walking a tree with Find
Find.find(dir) yields every path under a directory depth-first, and Find.prune skips the current directory, which Dir.glob("**/*") cannot do.
It also handles huge trees without building an array.
# frozen_string_literal: true
require "find"
require "fileutils"
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
%w[lib/a.rb lib/deep/b.rb node_modules/x.js].each do |name|
full = File.join(dir, name)
FileUtils.mkdir_p(File.dirname(full))
File.write(full, "")
end
found = []
Find.find(dir) do |path|
Find.prune if File.basename(path) == "node_modules"
found << path.delete_prefix("#{dir}/") if File.file?(path)
end
p found.sort
end
Output:
["lib/a.rb", "lib/deep/b.rb"]
Source: Ruby 4.0 documentation: Find
Pathname
Pathname wraps a path string in an object that answers the questions you would otherwise ask File, Dir, FileTest and
FileUtils, and that composes with / and join. Its aim is readability: root / "data" / "books.csv" instead of File.join(root, "data", "books.csv").
Ruby 4.0 promoted Pathname from a default gem to a core class (Feature #17473), so Pathname.new("x") and the Pathname("x") conversion
method work without require. Existing require "pathname" lines are harmless and still needed on Ruby 3.4 and older. In 4.0 two
methods, Pathname#find and Pathname#rmtree, still come from the library file and need the require (otherwise rmtree raises
NoMethodError). RuboCop with target 4.0 reports the require as redundant, so add it with an explanatory comment when you use those two.
# frozen_string_literal: true
# No require "pathname" is needed in Ruby 4.0.
root = Pathname.new("/srv/shelf")
data = root / "data" / "books.csv"
puts data
p data
puts data.basename, data.dirname, data.extname
puts data.sub_ext(".json")
puts data.parent.parent
puts root + Pathname("../etc")
puts root.join("/abs")
puts Pathname("a//b/../c/./d").cleanpath
puts data.relative_path_from(Pathname("/srv"))
puts data.absolute?, Pathname("x").relative?
p data.each_filename.to_a
p data.descend.map(&:to_s)
p Pathname("a/b/c").ascend.map(&:to_s)
puts Pathname("a/b.rb").to_path.class
puts data.sub("books", "authors")
puts data.fnmatch?("*.csv")
puts Pathname("a") <=> Pathname("b")
same = Pathname("a")
puts same == Pathname("a")
Output:
/srv/shelf/data/books.csv
#<Pathname:/srv/shelf/data/books.csv>
books.csv
/srv/shelf/data
.csv
/srv/shelf/data/books.json
/srv/shelf
/srv/etc
/abs
a/c/d
shelf/data/books.csv
true
true
["srv", "shelf", "data", "books.csv"]
["/", "/srv", "/srv/shelf", "/srv/shelf/data", "/srv/shelf/data/books.csv"]
["a/b/c", "a/b", "a"]
String
/srv/shelf/data/authors.csv
true
-1
true
Source: Ruby 4.0 documentation: Pathname
Points worth noting from that output: + and join follow path semantics ("/abs" as the right operand replaces the left side, and ..
is resolved against the left side), cleanpath simplifies without touching the disk (it can be wrong in the presence of symbolic links, use
realpath for that), and relative_path_from computes a relative path between two paths of the same kind. A Pathname is not a String: it has to_s
and to_path, and every File and Dir method accepts it because they call to_path. Pathname#== compares the strings, so a/b and a//b are
different pathnames until cleaned.
The file system half of the API mirrors the other classes:
| Pathname method | Equivalent to |
|---|---|
|
|
|
|
|
|
|
|
|
The class-level conveniences. |
# frozen_string_literal: true
require "pathname" # rubocop:disable Lint/RedundantRequireStatement -- rmtree and find need it
require "tmpdir"
Dir.mktmpdir("shelf") do |tmp|
dir = Pathname(tmp) / "shelf"
dir.mkpath
(dir / "b.txt").write("beta\n")
(dir / "a.txt").write("alpha\n")
(dir / "sub").mkdir
puts dir.exist?, dir.directory?, (dir / "a.txt").file?
puts (dir / "a.txt").read
p (dir / "b.txt").readlines(chomp: true)
p dir.children.map { |c| c.basename.to_s }.sort
p dir.children(false).map(&:to_s).sort
p dir.glob("*.txt").map { |c| c.basename.to_s }.sort
p Pathname.glob("*.txt", base: dir).map(&:to_s).sort
dir.each_child { |c| puts "child #{c.basename}" if c.directory? }
(dir / "a.txt").rename(dir / "z.txt")
puts (dir / "z.txt").size
puts dir.empty?
(dir / "sub").rmdir
dir.rmtree
puts dir.exist?
end
Output:
true
true
true
alpha
["beta"]
["a.txt", "b.txt", "sub"]
["a.txt", "b.txt", "sub"]
["a.txt", "b.txt"]
["a.txt", "b.txt"]
child sub
6
false
false
Source: Ruby 4.0 documentation: Pathname
Use Pathname for application code that builds and passes around paths; use plain strings when a path is a leaf value you only hand
to File.open. The two mix freely.
FileUtils
FileUtils is the library for operations on trees, modelled on shell commands. Every method accepts a single path or an array and
the options verbose: (print the equivalent shell command), noop: (do nothing; for a dry run) and, where it makes sense, force:, preserve:
and mode:. There are three companion modules, FileUtils::Verbose, FileUtils::NoWrite and FileUtils::DryRun, that you can
include to change the default for every call in a class.
| Method | Shell equivalent and notes |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
Symbolic links; |
|
Permission and ownership, recursively with |
|
Whether the two files have identical content. |
|
Copy with a mode, like |
|
|
|
Remove a file or tree; the |
# frozen_string_literal: true
require "fileutils"
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
Dir.chdir(dir) do
FileUtils.mkdir_p("out/reports/2026")
FileUtils.touch("out/reports/2026/a.txt")
FileUtils.cp("out/reports/2026/a.txt", "out/b.txt")
FileUtils.cp_r("out/reports", "out/backup")
FileUtils.mv("out/b.txt", "out/c.txt")
p Dir.glob("**/*", base: "out").sort
FileUtils.rm("out/c.txt")
FileUtils.rm_f("out/missing.txt") # no error when absent
begin
FileUtils.rm("out/missing.txt")
rescue Errno::ENOENT => e
puts e.class
end
FileUtils.rm_r("out/backup")
FileUtils.ln_s("reports", "out/latest")
puts File.symlink?("out/latest")
puts FileUtils.compare_file("out/reports/2026/a.txt", "out/reports/2026/a.txt")
FileUtils.rm_rf("out")
puts Dir.exist?("out")
end
end
Output:
["backup", "backup/2026", "backup/2026/a.txt", "c.txt", "reports", "reports/2026", "reports/2026/a.txt"]
Errno::ENOENT
true
true
false
# frozen_string_literal: true
require "fileutils"
require "tmpdir"
Dir.mktmpdir("shelf") do |dir|
out = File.join(dir, "build")
FileUtils.mkdir_p(out, verbose: true, noop: true)
puts Dir.exist?(out)
FileUtils.mkdir_p(out, mode: 0o750)
FileUtils.touch(File.join(out, "x"))
FileUtils.cd(out) { puts Dir.pwd == File.realpath(out) }
puts FileUtils.pwd.class
FileUtils.remove_entry_secure(out) # safe against races in world-writable dirs
puts Dir.exist?(out)
begin
FileUtils.remove_entry(out)
rescue Errno::ENOENT => e
puts e.class
end
end
Output:
mkdir -p /tmp/shelf20261018-xxxx/build
false
true
String
false
Errno::ENOENT
rm_rf and rm_r do exactly what they say: FileUtils.rm_rf("#{prefix}/#{name}") with an empty prefix and name deletes from the file system root
if permissions allow it. Validate the path (see the safety section) before deleting, prefer remove_entry on a path you just created,
and use noop: true, verbose: true while developing a cleanup script. cp and mv overwrite an existing destination without asking.
Temporary files and directories
Tempfile.create and Dir.mktmpdir are the supported ways to get scratch space. Both create the object with an unpredictable name
in the system temporary directory (Dir.tmpdir, which honors the TMPDIR environment variable), and Tempfile creates the file with
mode 0600, so other users cannot read or replace it. Both have a block form that deletes the object when the block ends, which should be
your default. Tempfile.new returns a Tempfile wrapper (a delegator to a File) whose file is deleted when the object is garbage collected
or at exit, unlink removes it early, and close! closes and deletes it in one step. The name can take a prefix and suffix as an array
(Tempfile.create(["books", ".csv"])). Tempfile.create(anonymous: true) (Ruby 3.4) creates a file that is removed immediately and is reachable only through the
open handle, so nothing can leak.
# frozen_string_literal: true
require "tempfile"
# Tempfile.create with a block removes the file when the block ends.
path = nil
Tempfile.create(["books", ".csv"]) do |file|
path = file.path
file.puts "isbn,title"
file.flush
puts File.basename(path).match?(/\Abooks.*\.csv\z/)
puts File.read(path)
end
puts File.exist?(path)
# Tempfile.new returns a wrapper that is removed by the garbage collector.
tmp = Tempfile.new("scratch")
tmp.write("data")
tmp.close
puts File.exist?(tmp.path)
tmp.open # reopen for reading
puts tmp.read
tmp.close! # close and unlink immediately
puts tmp.path.inspect
Tempfile.open("block") { |f| puts f.class }
Output:
true
isbn,title
false
true
data
nil
Tempfile
Source: Ruby 4.0 documentation: Tempfile
Tempfile.create returns a plain File, while Tempfile.new returns the delegating wrapper; prefer create with a block for new code, and use new when the
temporary file’s lifetime has to follow an object that is passed around. When another process or library needs the file by name, call flush
or close first so the data is on disk.
# frozen_string_literal: true
require "tmpdir"
kept = nil
Dir.mktmpdir(["shelf-", "-work"]) do |dir|
kept = dir
puts File.basename(dir).start_with?("shelf-")
File.write(File.join(dir, "a.txt"), "x")
end
puts Dir.exist?(kept)
# Without a block the directory stays until you remove it.
dir = Dir.mktmpdir("manual")
puts Dir.exist?(dir)
Dir.rmdir(dir)
puts Dir.tmpdir == File.realpath(Dir.tmpdir) || Dir.tmpdir.class
Output:
true
false
true
String
Source: Ruby 4.0 documentation: Tempfile; Dir.mktmpdir and Dir.tmpdir
belong to the tmpdir default library documented under Dir
Test suites should do all their file work in a Dir.mktmpdir directory (or use StringIO, next), which keeps tests independent and
leaves nothing behind. Never invent temporary names such as "/tmp/mydata.txt" or "/tmp/#{Process.pid}": they are predictable, so
another user can plant a file or symbolic link there first.
StringIO
StringIO implements the IO interface over a string: gets, each_line, read, write, puts, printf, <<, rewind,
seek, ungetc, eof? and so on. It is how you test code that reads or writes files, capture output, or feed a parser from a
string. It needs require "stringio". Differences from a real file: there is no file descriptor (fileno is nil, and it cannot go to
IO.select or a child process), ungetc and writes modify the underlying string in place, and a StringIO made from a frozen
string (which includes every string literal in a file with # frozen_string_literal: true) is read-only, so writes raise
IOError (not opened for writing) and ungetc raises IOError (not modifiable string). Use +"..." or String.new for a
writable buffer.
# frozen_string_literal: true
require "stringio"
# String literals are frozen here, so build a mutable copy for ungetc.
io = StringIO.new(+"one\ntwo\nthree\n")
puts io.gets.inspect
p io.each_line.map(&:chomp)
puts io.eof?
io.rewind
p io.read(3)
io.ungetc("X")
p io.read(4)
out = StringIO.new
out.puts "hello"
out.print "a", "b"
out << "!"
out.printf("%03d", 7)
p out.string
# A frozen string yields a read-only StringIO.
begin
StringIO.new("fixed").write("x")
rescue IOError => e
puts "#{e.class}: #{e.message}"
end
# Capture $stdout in a test.
original = $stdout
$stdout = StringIO.new
puts "captured"
captured = $stdout.string
$stdout = original
p captured
Output:
"one\n"
["two", "three"]
true
"one"
"X\ntw"
"hello\nab!007"
IOError: not opened for writing
"captured\n"
Source: Ruby 4.0 documentation: StringIO
Writing code against "anything with puts`" instead of against file names makes it testable. The shelf scenario shows the shape: the export
method takes the destination as an argument and does not care whether it is a `File, $stdout or a StringIO.
# frozen_string_literal: true
require "stringio"
Author = Data.define(:name)
Book = Data.define(:isbn, :title, :author) do
def to_s = "#{title} by #{author.name}"
end
# Writes any enumerable of books to any IO-like object.
def export_books(books, io)
books.each { |book| io.puts [book.isbn, book.title, book.author.name].join("\t") }
io
end
books = [Book.new("9780441172719", "Dune", Author.new("Frank Herbert")),
Book.new("9780140449136", "The Odyssey", Author.new("Homer"))]
sink = export_books(books, StringIO.new)
print sink.string
export_books(books.first(1), $stdout)
lines = StringIO.new(sink.string).each_line(chomp: true).map { |l| l.split("\t") }
p lines.first
Output (the shelf Book and Author value objects are repeated in trimmed form so the file runs on its own):
9780441172719 Dune Frank Herbert
9780140449136 The Odyssey Homer
9780441172719 Dune Frank Herbert
["9780441172719", "Dune", "Frank Herbert"]
Source: Ruby 4.0 documentation: StringIO
IO::Buffer
IO::Buffer (Ruby 3.1) is a fixed-size block of bytes, either owned memory (IO::Buffer.new(size)), a view over a string
(IO::Buffer.for(string), read-only for a frozen string) or a memory-mapped file (IO::Buffer.map(file)). You read and write typed values by
offset (get_value(:U32, offset), set_value, get_string, set_string; the type symbols include :U8, :S8, :U16 for big-endian and
:u16 for little-endian, :U32, :U64, :F32, :F64 and so on), slice it without copying, compare, XOR and copy between buffers, and
read from or write to an IO with read, write, pread and pwrite. It is the zero-copy layer under the fiber scheduler’s io_read
and io_write hooks (see Fibers and the Fiber Scheduler).
Ruby 3.4 lets IO::Buffer#copy release the GVL for large copies. The class still prints an experimental warning when first used, because
its interface may change: this example turns off that one warning category with Warning[:experimental] = false. For everyday code
you almost never need it; use strings, pack and unpack.
# frozen_string_literal: true
# IO::Buffer is still marked experimental; silence only that category.
Warning[:experimental] = false
buffer = IO::Buffer.new(16)
buffer.set_string("shelf", 0)
buffer.set_value(:U32, 8, 0xCAFEBABE)
puts buffer.get_string(0, 5)
puts format("%#x", buffer.get_value(:U32, 8))
puts buffer.size
puts buffer.external?, buffer.readonly?
view = IO::Buffer.for("read only text")
puts view.readonly?
puts view.get_string(5, 4)
begin
view.set_string("X")
rescue IO::Buffer::AccessError => e
puts "#{e.class}: #{e.message}"
end
slice = buffer.slice(0, 5)
slice.set_string("SHELF")
puts buffer.get_string(0, 5)
p buffer.each(:U8, 0, 3).to_a
Output:
shelf
0xcafebabe
16
false
false
true
only
IO::Buffer::AccessError: Buffer is not writable!
SHELF
[[0, 83], [1, 72], [2, 69]]
Pipes, readiness and locks
Pipes and non-blocking reads
IO.pipe returns a connected reader and writer: whatever one thread or process writes, the other reads. The reader sees end of file
only after every copy of the writer is closed, a common source of hangs when a forked child still holds one. A blocking read or
gets waits for data. IO#wait_readable(timeout) waits until a stream is ready (and returns nil on timeout), IO#read_nonblock(n)
never blocks and raises IO::WaitReadable (IO::EAGAINWaitReadable) when nothing is available, unless you pass exception: false, in which case it
returns :wait_readable; at the end of file it raises EOFError or returns nil with exception: false. write_nonblock is the mirror. IO.select(readers,
writers, errors, timeout) waits on several streams at once and returns the ready ones (nil on timeout; since 4.0 the timeout may be Float::INFINITY).
Under a fiber scheduler these calls yield to other fibers instead of blocking the thread.
# frozen_string_literal: true
reader, writer = IO.pipe
writer.puts "from the writer"
writer.close
puts reader.read
reader.close
r, w = IO.pipe
p r.wait_readable(0.1) # nothing to read yet
w.write "x"
puts r.wait_readable(0.1) == r
puts r.read_nonblock(10)
begin
r.read_nonblock(10)
rescue IO::WaitReadable => e
puts e.class
end
p r.read_nonblock(10, exception: false)
w.close
p r.read_nonblock(10, exception: false) # nil at end of file
r.close
# IO.select waits on several IO objects at once.
a_r, a_w = IO.pipe
b_r, b_w = IO.pipe
b_w.write "b is ready"
ready, = IO.select([a_r, b_r], nil, nil, 0.1)
puts ready.map { |io| io == b_r }.inspect
[a_r, a_w, b_r, b_w].each(&:close)
Output:
from the writer
nil
true
x
IO::EAGAINWaitReadable
:wait_readable
nil
[true]
Source: Ruby 4.0 documentation: IO
Locking and atomic replacement
File#flock takes an advisory lock: File::LOCK_SH (shared), File::LOCK_EX (exclusive), optionally ORed with File::LOCK_NB to return false
instead of waiting, and File::LOCK_UN to release. Advisory means that it only constrains programs that also call flock; a program that ignores locking
can still write. The lock is released when the file is closed, which is another reason to use the block form. Never truncate a file before locking it: open
without "w", lock, and then truncate.
To replace a file so that readers see either the old or the new content and never a partial file, write a temporary file in the same directory (a rename is atomic only inside one file system) and rename it over the target.
# frozen_string_literal: true
require "tmpdir"
# Replace a file atomically: write a sibling temp file, then rename over it.
def write_atomically(path, content)
tmp = "#{path}.#{Process.pid}.tmp"
File.write(tmp, content)
File.rename(tmp, path)
end
Dir.mktmpdir("shelf") do |dir|
path = File.join(dir, "catalog.txt")
write_atomically(path, "v1\n")
write_atomically(path, "v2\n")
puts File.read(path)
# Advisory locking with flock; the lock is released on close.
File.open(path, File::RDWR | File::CREAT) do |file|
file.flock(File::LOCK_EX)
file.truncate(0)
file.write("locked write\n")
file.flush
File.open(path) { |other| puts other.flock(File::LOCK_SH | File::LOCK_NB) }
end
puts File.read(path)
end
Output:
v2
false
locked write
Source: Ruby 4.0 documentation: File
Errors
File operations raise Errno:: classes, all subclasses of SystemCallError, which carry the C error name, the message from the operating system, the
function that failed (@ rb_sysopen) and the path. SystemCallError#errno returns the number. IOError signals a misuse of the stream (closed, not opened
for that direction) and EOFError, a subclass of IOError, signals reading past the end. Rescue the narrowest class that you can handle and let the rest propagate.
| Exception | Typical cause |
|---|---|
|
The file or a parent directory does not exist (also deleting a missing file). |
|
Creating something that exists ( |
|
Not allowed: permission bits, or a protected operation. |
|
A directory where a file was expected, or a file used as a directory component. |
|
|
|
|
|
Disk full or quota exceeded; surfaces on |
|
Too many open files: usually a descriptor leak (missing |
|
Writing to a pipe whose reader has gone. |
|
A non-blocking call would block; raised as |
|
Programming errors on the stream. |
|
|
|
A file name containing |
# frozen_string_literal: true
require "tmpdir"
def try
yield
rescue SystemCallError, IOError => e
puts "#{e.class} < #{e.class.superclass}: #{e.message.gsub(%r{/[^ ]*/}, '<dir>/')}"
end
def read_twice(file)
File.open(file) do |f|
f.read
f.readline
end
end
Dir.mktmpdir("shelf") do |dir|
file = File.join(dir, "f.txt")
File.write(file, "x")
try { File.read(File.join(dir, "nope")) }
try { File.read(dir) }
try { Dir.mkdir(dir) }
try { Dir.rmdir(dir) }
try { File.read(File.join(file, "child")) }
try { File.open(file, "r") { |f| f.write("y") } }
try { File.open(file, "w", &:read) }
try { read_twice(file) }
File.chmod(0o000, file)
try { File.read(file) }
puts Errno::ENOENT::Errno, Errno::ENOENT.new.errno
puts Errno::EAGAIN == Errno::EWOULDBLOCK
end
Output:
Errno::ENOENT < SystemCallError: No such file or directory @ rb_sysopen - <dir>/nope
Errno::EISDIR < SystemCallError: Is a directory @ io_fread - <dir>/shelf20261018-xxxx
Errno::EEXIST < SystemCallError: File exists @ dir_s_mkdir - <dir>/shelf20261018-xxxx
Errno::ENOTEMPTY < SystemCallError: Directory not empty @ dir_s_rmdir - <dir>/shelf20261018-xxxx
Errno::ENOTDIR < SystemCallError: Not a directory @ rb_sysopen - <dir>/child
IOError < StandardError: not opened for writing
IOError < StandardError: not opened for reading
EOFError < IOError: end of file reached
Errno::EACCES < SystemCallError: Permission denied @ rb_sysopen - <dir>/f.txt
2
2
true
Source: Ruby 4.0 documentation: Errno and SystemCallError
The last example is an application of all this to the shelf scenario: a small class that saves books as tab-separated lines through a
temporary file and rename, and reads them lazily with Pathname#each_line, building the same validated Isbn objects as the rest of the gem. A missing
file surfaces as Errno::ENOENT, which the caller decides how to handle. It needs the shelf gem on the load path (ruby -I lib).
# frozen_string_literal: true
require "shelf"
require "tmpdir"
# Persists a catalogue as one tab-separated line per book.
class BookFile
def initialize(path) = @path = Pathname(path)
def save(books)
tmp = @path.sub_ext(".tmp")
tmp.open("w") do |io|
books.each { |b| io.puts [b.isbn, b.title, b.author.name].join("\t") }
end
tmp.rename(@path)
end
def each_book
return enum_for(:each_book) unless block_given?
@path.each_line(chomp: true) do |line|
isbn, title, name = line.split("\t")
yield Shelf::Book.new(Shelf::Isbn.new(isbn), title, Shelf::Author.new(name))
end
end
end
Dir.mktmpdir("shelf") do |dir|
file = BookFile.new(File.join(dir, "books.tsv"))
dune = Shelf::Book.new(Shelf::Isbn.new("978-0-441-17271-9"), "Dune", Shelf::Author.new("Frank Herbert"))
file.save([dune])
puts file.each_book.map(&:to_s)
puts File.read(File.join(dir, "books.tsv")).inspect
begin
BookFile.new(File.join(dir, "missing.tsv")).each_book.to_a
rescue Errno::ENOENT => e
puts e.class
end
end
Output:
Dune by Frank Herbert
"9780441172719\tDune\tFrank Herbert\n"
Errno::ENOENT
Source: Ruby 4.0 documentation: Pathname
Safe handling of paths
-
Never trust a name from outside. A user-supplied name such as
"../../etc/passwd"or an absolute path escapes the directory you meant. Resolve the candidate against the root withFile.expand_pathand check that the result still starts with the root plus the separator.File.join(root, name)alone does not stop.., and an absolutenameis not safe withPathname#joinorPathname#+(it replaces the root). Userealpathwhen symbolic links are involved, because a link inside the root can point outside it. -
A file name containing a null byte raises
ArgumentErrorbefore the system call, so the classic null-byte truncation trick fails in Ruby. -
Do not interpolate names into shell strings; pass an argument array to
system,spawnorOpen3, see Processes, Signals and Shell. -
Create files with the narrowest mode they need (
0o600for secrets) and exclusively ("wx") when overwriting would be a bug. -
Use
TempfileandDir.mktmpdir, not predictable names, andremove_entry_securewhen deleting in shared directories. -
Load data from files with the safe loaders (
JSON.parse,YAML.safe_load);Marshal.loadandYAML.unsafe_loadon a file someone else wrote can execute code, see Security.
# frozen_string_literal: true
require "tmpdir"
# Resolve a user-supplied name inside a root directory, or return nil.
def inside(root, name)
base = File.realpath(root)
full = File.expand_path(name, base)
full if full.start_with?("#{base}#{File::SEPARATOR}")
end
Dir.mktmpdir("shelf") do |root|
puts inside(root, "books.csv").sub(File.realpath(root), "<root>")
p inside(root, "../etc/passwd")
p inside(root, "/etc/passwd")
begin
File.read("books\0.csv")
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
end
Output:
<root>/books.csv
nil
nil
ArgumentError: path name contains null byte
Source: Ruby 4.0 documentation: File
Pitfalls and a decision guide
| Situation | Do this |
|---|---|
Reading or writing a small file in one go |
|
Processing a big file line by line |
|
f |
f.each_line … }`. |
Several operations on one open file |
`+File.open(path, mode) { |
file |
… }+ |
Binary data |
|
A file that may have a byte order mark |
|
Copying a file or stream |
|
Building and passing paths in application code |
|
Finding files by pattern |
|
Recursive create, copy, move, delete |
|
Scratch files and directories |
|
Testing code that does I/O |
Pass an object with |
Replacing a file safely |
Write a sibling temporary file, then |
Several processes updating one file |
|
Waiting on several streams |
|
Deleting from a computed path |
Validate the path first; |
Common pitfalls:
-
File.openwithout a block and nocloseleaks a descriptor; a long-running process then fails withErrno::EMFILE. -
File.readlinesandFile.readon a huge file exhaust memory; stream instead. -
File.writeoverwrites; usemode: "a"to append. -
getsin a script readsARGF, not standard input, when arguments are present. -
Dir.childrenandDir.glob(sort: false)have no stable order, andDir.childrenreturns names rather than paths. -
Dir.chdiris process-wide and raises in a second thread’s block; build absolute paths instead. -
Dir.globdoes not match hidden files unlessFile::FNM_DOTMATCHis given, and*never crosses a directory separator. -
Output to a pipe is buffered; set
$stdout.sync = trueorflushwhen timing matters. -
An exception during a write can leave a partial file: replace atomically for anything important.
-
Text read on Unix keeps
\r; usechomp: trueor"rt"for files that may come from Windows. -
A
StringIOover a frozen literal is read-only. -
Tempfile.newfiles are removed when the object is collected: keep a reference for as long as another process needs the name.
Where older material is outdated
| Older code and tutorials | Ruby 4.0 |
|---|---|
|
Removed in 3.2; use |
|
Deprecated in 3.3, removed in 4.0 ( |
|
Not needed in 4.0 (except for |
|
Sorted by default since 3.0; |
|
Prefer the block form or the one-call helpers. |
Examples that call |
These methods do not exist in Ruby; use |
|
|
Temporary files named by hand |
|
|
Available on |
|
|
Version notes
| Version | Change |
|---|---|
3.0 |
|
3.1 |
|
3.2 |
|
3.3 |
File-open shortcuts that spawn processes deprecated; |
3.4 |
|
4.0 |
|