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: true at the top of a file avoids allocating a new string per evaluation.
  • bytesize differs from length for any non-ASCII text, which matters when enforcing a database column limit.
  • sub and gsub return new strings; the ! versions mutate the receiver and return nil when nothing changed - so str.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")
MethodBehaviourUse
File.readWhole file into memorySmall files, config
File.foreachLine by lineLogs, large CSV
File.readlinesArray of lines in memorySmall files needing random access
IO.copy_streamKernel-level copyMoving big files without Ruby loops
File.open with a blockCloses on exit, even on raiseAny 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.parse raises JSON::ParserError on malformed input; rescue it and return a clear error rather than letting the stack trace reach the user.
  • Never call JSON.parse with create_additions: true on untrusted input - it can instantiate arbitrary classes.
  • Print diagnostics to $stderr and 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.

Collections, blocks and Enumerable Packaging and distributing gems and CLIs

Last refreshed 2026-09-18.