Strings, files and input/output
String methods and encoding, interpolation and heredocs, File and IO, CSV and JSON, ARGV, and streaming large files without loading them.
Strings and encoding
name = "Ada Lovelace"
"Hello, #{name.upcase}" # interpolation calls to_s
%w[one two three].join(", ")
%q(no #{interpolation} here) # single-quote semantics
%Q(yes #{name} here)
name.strip.downcase.tr(" ", "_")
name.sub(/A(w+)/) { $1.downcase }
name.gsub(/[aeiou]/, "*")
"a,b,,c".split(",", -1) # keeps trailing empty fields
"42".rjust(6, "0") # => "000042"
"hello".center(11, "-")
"café".encoding # => Encoding:UTF-8
"café".bytesize # bytes, not characters
str = "x".dup.force_encoding("BINARY")
str.valid_encoding?- A literal is UTF-8 by default;
# frozen_string_literal: trueat the top of a file avoids allocating a new string per evaluation. bytesizediffers fromlengthfor any non-ASCII text, which matters when enforcing a database column limit.subandgsubreturn new strings; the!versions mutate the receiver and return nil when nothing changed - sostr.gsub!(...)in a chain can raise on nil.- Prefer
String#start_with?to a regex when the pattern is a fixed prefix: no regex engine, no backtracking.
⚠️
Never build SQL by interpolating strings.
"where name = '#{name}'" is an injection hole; pass bind parameters to the driver instead, or use the ORM.Files and heredocs
report = <<~TEXT
Daily report
------------
Revenue: #{revenue}
Orders: #{orders}
TEXT
File.write("out/report.txt", report)
# read a whole small file
config = File.read("config.yml")
# stream a large file: constant memory
File.foreach("huge.log").with_index(1) do |line, no|
puts "#{no}: #{line}" if line.include?("ERROR")
end
# explicit handle, always closed
File.open("data.csv", "w") do |f|
f.puts "id,title"
rows.each { |r| f.puts [r[:id], r[:title]].join(",") }
end
require "pathname"
root = Pathname.new(__dir__).parent
(root + "tmp").mkpath
(root + "tmp" + "run.log").write("started")| Method | Behaviour | Use |
|---|---|---|
File.read | Whole file into memory | Small files, config |
File.foreach | Line by line | Logs, large CSV |
File.readlines | Array of lines in memory | Small files needing random access |
IO.copy_stream | Kernel-level copy | Moving big files without Ruby loops |
File.open with a block | Closes on exit, even on raise | Any write |
Use File.open with a block rather than a bare f = File.open: the block form closes the handle when the block exits, including on an exception. Open a file in binary mode when you only move bytes - text mode on Windows rewrites line endings.
CSV, JSON and ARGV
require "csv"
CSV.foreach("books.csv", headers: true) do |row|
puts row["title"]
# row["title"] is nil for a missing header: guard the expected schema
end
CSV.open("out.csv", "w") do |csv|
csv << %w[id title author]
books.each { |b| csv << [b[:id], b[:title], b[:author]] }
end
require "json"
data = JSON.parse(File.read("payload.json"))
File.write("out.json", JSON.pretty_generate(data))
# strict parsing: symbols keys, but reject unknown fields
schema = JSON.parse(payload, symbolize_names: true)
# comment: max_nesting and create_additions are off by default and should stay off
# CLI arguments
if ARGV.empty?
warn "usage: report.rb INPUT [--limit N]"
exit 1
end
input = ARGV.shift
limit = ARGV.each_slice(2).find { |k, _| k == "--limit" }&.last&.to_i || 100
STDERR.puts "processing #{input}"JSON.parseraisesJSON::ParserErroron malformed input; rescue it and return a clear error rather than letting the stack trace reach the user.- Never call
JSON.parsewithcreate_additions: trueon untrusted input - it can instantiate arbitrary classes. - Print diagnostics to
$stderrand results to$stdout, so a CLI can be piped while still reporting progress. - CSV destroys the distinction between an empty string and a missing field; validate the header once at the start.
FAQ
Why is my file read returning an unexpected encoding error?
The bytes are not valid UTF-8, usually because the file is Latin-1 or a binary format. Check with
valid_encoding? and either force the encoding at the boundary or read in binary mode.Should I use File or Pathname?
Pathname for constructing and normalising paths, especially with
File.join-style concatenation you want to avoid. File or IO for the actual read and write operations.Related
Collections, blocks and Enumerable Packaging and distributing gems and CLIs
Last refreshed 2026-09-18.