File
The File module provides utilities for working with files and directories in Ruby. It is part of the standard library and offers methods for reading, writing, and managing file paths.
When you work with files in Ruby, the same module usually handles the whole flow: open something, inspect it, transform it, and write it back out if needed. That makes File a good place to start when you want straightforward filesystem code without pulling in extra libraries.
Overview
The File class is Ruby’s interface to the operating system file system. It provides methods for:
- Reading and writing file contents
- Getting and setting file metadata
- Working with file paths
- Checking file existence and permissions
opening and reading files
File.read reads the entire contents of a file into a string.
Use this when you want the full file at once, such as a small config file or a template. If the file might be large, you may prefer line-based iteration so you can process each line as it comes in.
contents = File.read("config.txt")
puts contents
File.read loads the entire file into a single string in one call, which is convenient for small files like configuration or templates. The method is simple but memory-hungry for large files — if the file is hundreds of megabytes, reading it all at once can cause your Ruby process to balloon. For bigger inputs, line-by-line iteration or buffered reading is safer.
File.read Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
filename | String | Required | Path to the file |
length | Integer | nil | Number of bytes to read |
offset | Integer | nil | Starting position |
mode | String | "r" | File mode |
The optional length and offset parameters let you read a specific slice of a file without loading the whole thing. Passing length: 1024 with offset: 0 reads only the first kilobyte. This is useful when you know the data you need sits at a fixed position, such as a binary header or a known byte range in a structured format.
reading line by line
File.foreach iterates through a file line by line:
File.foreach("log.txt") do |line|
puts line
end
File.foreach streams the file one line at a time, never holding more than a single line in memory. This makes it the right choice for large log files, CSV exports, or any file where the total size could be measured in gigabytes. The block receives each line including its trailing newline, so you may want to call chomp if you are processing the text further.
Or use File.readlines to get an array of lines when you want to work with the whole list after reading it:
lines = File.readlines("data.csv")
lines.each { |line| puts line }
File.readlines reads every line into an array, trading memory for random access. Once the lines are in an array you can index into them, reverse them, or filter them with select — operations that are impossible with the streaming foreach approach. For files of moderate size, the convenience of having all lines available as an array often outweighs the memory cost.
Writing Files
File.write writes content to a file:
This is the simplest write path in Ruby. It is useful for small outputs, temporary artifacts, or any case where you already have the final string ready to save. The method opens the file, writes the content, and closes it in one atomic-looking operation, so you do not need to manage the file handle yourself.
File.write("output.txt", "Hello, World!")
File.write Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
filename | String | Required | Path to the file |
data | String | Required | Content to write |
mode | Integer | nil | File mode (permissions) |
The optional mode parameter sets Unix-style file permissions when the file is created. Pass an octal literal like 0644 to make the file owner-writable and world-readable. If the file already exists, File.write truncates it before writing — use append mode via File.open with "a" if you need to preserve existing content.
writing with open block
File.open with a block automatically closes the file:
The block form is safer for longer operations because Ruby closes the file for you even if the block raises an error. That means you do not have to remember a separate cleanup step. The block also receives the File object, so you can call methods like puts, print, or write directly on it — each call appends to the open file handle until the block exits. Closing the file promptly is important because operating systems limit the number of open file handles per process, and leaked handles can cause “too many open files” errors in long-running programs.
File.open("output.txt", "w") do |file|
file.puts "First line"
file.puts "Second line"
end
file modes
| Mode | Description |
|---|---|
| ”r” | Read only (default) |
| “w” | Write only, truncates file |
| ”a” | Append mode |
| ”r+“ | Read and write |
| ”w+“ | Read and write, truncates |
| ”a+“ | Read and append |
# Append to existing file
File.open("log.txt", "a") do |file|
file.puts "New log entry"
end
Append mode ("a") opens the file for writing but positions the cursor at the end, so new content is added without overwriting what is already there. This is the standard mode for log files, audit trails, and any write-once-append-only use case. If the file does not exist, Ruby creates it automatically — append mode never truncates.
Working with Paths
File.join combines path components:
Path helpers are especially useful when your code runs on more than one operating system. They keep separators consistent and make it easier to build paths from individual pieces instead of hard-coding a string. File.join uses the correct separator (/ on Unix, \ on Windows) for whichever platform the code is running on, so paths built this way are portable without conditional logic.
path = File.join("home", "user", "documents", "file.txt")
# => "home/user/documents/file.txt" (Unix)
File.join accepts any number of string arguments and joins them with the platform-appropriate path separator. It also normalises redundant separators, so File.join("home/", "/user") correctly produces "home/user" without a double slash. This makes it safer than manual string concatenation, especially when path segments come from user input or configuration files where leading or trailing slashes are unpredictable.
File.dirname gets the directory part:
File.dirname("/home/user/documents/file.txt")
# => "/home/user/documents"
dirname strips the last component from a path and returns everything before it. This is useful for finding the containing directory or resolving relative paths against a known location. The return value does not include a trailing slash.
File.basename gets the filename:
File.basename("/home/user/documents/file.txt")
# => "file.txt"
File.basename("/home/user/documents/file.txt", ".txt")
# => "file"
basename extracts just the filename from a full path. When you pass a second argument, it strips that suffix from the result — handy for getting a filename without its extension. This is commonly used in file-processing scripts where you need the base name for generating output filenames.
File.extname gets the extension:
File.extname("document.pdf")
# => ".pdf"
file information
File.exist? checks if a file exists:
These predicates are often the first guard you add before opening a file or changing it. They let you fail fast or branch to a fallback path when the expected file is not there.
File.exist?("config.txt") # => true or false
File.exist? is the most basic file check — it returns true for anything that exists on the filesystem, including directories, symlinks, and special device files. It is the Ruby equivalent of test -e in shell scripting. Use it as the first guard before attempting to open or read a file.
File.file? checks if it is a regular file (not directory):
File.file?("config.txt") # => true or false
File.file? is more specific than exist?: it returns true only for regular files, not directories, symlinks, or devices. This distinction matters when a path might be a directory with the same name, or when you are iterating over a mixed directory listing and only want to process actual files.
File.directory? checks if it is a directory:
File.directory?("folder") # => true or false
File.directory? returns true only when the path refers to an actual directory. It differs from File.exist? in that a symlink to a directory returns true for exist? but only true for directory? if the symlink target is itself a directory. This nuance matters when traversing filesystem trees where symlinks are common.
getting file size
File.size("large_file.bin") # => 1048576 (bytes)
File.size returns the file size in bytes as an integer. It raises Errno::ENOENT if the file does not exist, so wrap it in a guard or rescue block when checking files that may be absent. For very large files, the return value may exceed the range of a 32-bit integer, but Ruby’s arbitrary-precision integers handle that automatically.
getting modification time
File.mtime("config.txt")
# => 2026-03-13 10:30:00 +0000
# Compare modification times
file1 = "file1.txt"
file2 = "file2.txt"
File.mtime(file1) > File.mtime(file2) # => true/false
File.mtime returns a Time object representing the file’s last modification timestamp. Comparing two modification times with > works because Ruby’s Time objects support comparison operators directly. This is useful for build scripts that need to determine which files have changed since a reference timestamp.
File Permissions
File.chmod changes file permissions:
Permission helpers are most useful in deployment scripts and local tooling. They let you set access explicitly instead of relying on defaults that may vary across environments.
File.chmod(0644, "script.rb") # Read/write for owner, read for others
File.chmod changes the Unix permissions of an existing file. The octal value 0644 sets the owner to read/write and everyone else to read-only. Ruby also accepts symbolic permission strings like "u+x", but the octal form is more common in scripts because it sets all bits at once and is easier to compare against ls -l output.
File.readable? checks if readable:
File.readable?("config.txt") # => true or false
File.readable? returns true if the current process has permission to read the file. This depends on the file’s permission bits and the user the Ruby process runs under — not the file’s existence. A file can exist but not be readable, or a path can be readable but point to a non-existent file (in which case exist? would catch it first).
File.writable? checks if writable:
File.writable?("config.txt") # => true or false
File.writable? checks whether the current process can write to the file, considering both the file’s permission bits and the user the Ruby process runs under. A file owned by root with mode 0644 will return false for writable? when the process runs as a normal user, even though the owner bits permit writing.
File.executable? checks if executable:
File.executable?("script.sh") # => true or false
File.executable? returns true when the file has the execute permission bit set for the current process. On Unix systems, a script without a shebang line may still not be directly executable even if the permission bit is set, so combine this check with knowledge of how the file will be invoked.
deleting files
File.delete or File.unlink removes a file:
Use deletion carefully, especially in scripts that process user data. A small guard or a confirmation step can save you from removing the wrong path. Both delete and unlink raise Errno::ENOENT if the file does not exist, so check with File.exist? first or rescue the exception if the file may legitimately be absent.
File.delete("temp.txt")
File.unlink("temporary.dat")
Both methods are irreversible within the Ruby process — there is no undo or trash recovery. For scripts that delete files in batch, consider logging each deletion or moving files to a temporary holding directory first. A common safety pattern is to check File.exist? and confirm the path is within an expected directory before calling delete, especially when the path comes from user input or external configuration.
Practical Examples
The examples below show how the same module can cover several everyday tasks. Reading, counting, and writing all fit together naturally because the File API keeps the surface area small and consistent.
reading CSV files
require "csv"
File.foreach("data.csv") do |line|
puts CSV.parse_line(line).inspect
end
Using foreach with CSV is memory-efficient because only one line is in memory at a time. The CSV.parse_line method converts a single raw CSV line into an array of fields. For large CSV files with millions of rows, this streaming approach is essential — loading everything with File.readlines would allocate an array large enough to hold every parsed row.
processing log files
error_count = 0
File.foreach("application.log") do |line|
if line.include?("ERROR")
error_count += 1
end
end
puts "Found #{error_count} errors"
This example counts error lines in a log file without loading the entire file into memory. foreach streams each line, and the counter accumulates the number of ERROR matches. For large production logs that may be hundreds of megabytes, this streaming pattern keeps memory usage constant regardless of file size — only one line is held in memory at any time.
safely writing configuration
# Write atomically using a temp file
require "tempfile"
temp = Tempfile.new("config")
temp.write(YAML.dump(config))
temp.close
FileUtils.mv(temp.path, "config.yml")
Writing to a temporary file and then renaming it to the target path is the standard atomic-write pattern. If the process crashes while writing the temp file, the original config.yml remains untouched because the rename (mv) is a single filesystem operation. This guarantees that readers of config.yml never see a partially written file — they see either the old version or the complete new version, nothing in between.