rubyguides

String#each_line

str.each_line(separator=$/) { |line| block } -> str

The each_line method splits a string or reads from an IO object and iterates over each line. It’s the foundational method for line-by-line text processing.

With Strings

# Basic iteration
text = "line1\nline2\nline3"
text.each_line { |l| puts l }

# Output:
# line1
# line2
# line3

each_line splits at newline characters by default and yields each segment to the block, including the trailing newline. Call chomp inside the block or chain .chomp on the enumerator to strip line endings. The default separator is stored in the global $/ variable which defaults to "\n".

With custom separators

# Split by comma
csv = "a,b,c,d"
csv.each_line(",") { |s| puts s }
# a
# b
# c
# d

# Split by paragraph (empty string)
text.each_line("") { |p| puts p }

Passing a custom separator lets you treat any delimiter as the record boundary. A comma separates CSV-like data, while an empty string splits on paragraph breaks. The separator can be any string, which makes each_line flexible enough to handle custom record formats without writing a manual parsing loop.

Practical Examples

File Processing

# Each line from string
content = File.read("data.txt")
content.each_line do |line|
  line.chomp!
  process(line)
end

Reading the entire file into memory first works for small files but can be heavy for large ones. The block processes each line independently, with chomp! stripping the trailing newline so downstream logic sees clean text. For large files, prefer File.foreach which streams from disk without loading everything at once.

Line number tracking

text = "one\ntwo\nthree"
text.each_line.with_index do |line, i|
  puts "#{i + 1}: #{line}"
end
# 1: one
# 2: two
# 3: three

Chaining with_index after each_line adds a zero-based index to each yielded line. This is useful for error messages that cite a line number or for position-dependent logic. The index comes from the enumerator, not from parsing the string content.

Filtering Lines

text = "error: file not found\ninfo: starting\nerror: timeout"

errors = []
text.each_line do |line|
  errors << line if line.start_with?("error:")
end

The block checks each line against a prefix and collects matches in an array. This pattern appears often in log parsing where you want to extract specific severity levels or event types. The trailing newline stays on each collected line, so downstream processing may need to call chomp before further work.

With Chomp

# Common pattern
text.each_line.chomp.each { |line| ... }

Chaining chomp between each_line and each creates an enumerator that strips trailing newlines from every line before yielding. The chained call reads left to right as a pipeline: split into lines, remove newlines, then process each result. This keeps the block body clean of cleanup logic.

Return Values

# Returns string if block given
result = "a\nb\nc".each_line { |l| l.upcase }
# => "a\nb\nc"

# Returns enumerator if no block
enum = "a\nb\nc".each_line
enum.first  # => "a"

When a block is given, each_line returns the original string regardless of what the block does, enabling method chaining. Without a block, it returns an Enumerator, letting you compose it with map, select, or other collection operations in a lazy fashion without immediate evaluation.

Alternative Separators

# Custom separators
"word1|word2|word3".each_line("|") { |w| puts w }
# word1
# word2
# word3

Using "|" as a separator splits the string at pipe characters instead of newlines. This is useful for processing delimited data formats where records are separated by a character other than a newline. The same method adapts to CSVs, pipe-delimited logs, or any format with a consistent record boundary.

Memory Efficiency

# For large files, use File.each_line (streams)
File.each_line("huge.txt") do |line|
  process(line)  # One line at a time
end  # Doesn't load entire file

File.each_line reads from disk incrementally, keeping only one line in memory at a time. This is the right choice for log files, CSV exports, and any input larger than available RAM. The block-based interface is the same as String#each_line, so switching between in-memory and file-backed processing requires minimal code changes.

Comparison

# each_line = each (default separator)
"a\nb".each_line == "a\nb".each

This method is essential for text processing, log analysis, and any task involving line-oriented data.

Picking the right separator

The right separator choice depends on the data format. Newlines work for line-oriented text, commas for CSV data, and pipes or tabs for custom delimiters. The method stays the same; only the separator argument changes.

# Parse a simple key-value log format
log = "time=10:00 level=info msg=started
"
pairs = log.each_line(" ")  # split on spaces for key=value pairs

each_line is flexible because the separator is not limited to a newline. That makes it a good fit for logs, CSV-like text, or custom record formats where one logical item spans more than one physical line. When you only need to inspect each record, keep the block simple and avoid building large arrays first. If you do need line numbers or transformation, combine it with with_index or map so the loop body stays easy to scan.

The method also pairs well with chomp when you want to remove line endings and keep the text tidy before processing it further. In scripts that touch large inputs, line-by-line work helps control memory use and makes it obvious where each record starts and ends. That simplicity is often more valuable than a one-shot conversion into an array.

When you reach for each_line, think about the shape of the text first. If the records already have a natural separator, the method keeps the code close to the data and avoids extra indexing or manual slicing. It is also a good match for small filters, where the block can decide quickly whether to keep, skip, or transform a line without building extra state.

It is also easy to combine with simple transforms such as strip, chomp, or a small case statement inside the block. That keeps the line handling local to the point where the text is read, which makes log processors and file cleaners easier to maintain. When the records are already line shaped, the method usually feels like the most direct path.

See Also