Question

Read File Lines in Ruby: Handle LF, CRLF, and CR Endings

rubyline-endings

Question

How can I read a text file line by line in Ruby when the input may use different line-ending styles?

This code works for some files, but for others Ruby appears to read the entire file as one line:

line_num = 0

File.open("xxx.txt").each do |line|
  print "#{line_num += 1} #{line}"
end

The program must also support standard input, such as:

ruby my_prog.rb < file.txt

Because the input file may use LF, CRLF, or CR line endings, how can Ruby split the input into lines reliably?

Short Answer

You will learn how Ruby decides where one line ends, why files with older or different operating-system line endings can appear as one long line, and how to normalize newline formats when reading from files or STDIN.

Concept

Ruby reads text one record at a time using an input record separator. By default, that separator is a line feed character ("\n").

Common newline formats are:

  • LF (\n) — common on Linux and modern macOS.
  • CRLF (\r\n) — common on Windows.
  • CR (\r) — used by classic Mac OS files and occasionally encountered in exported data.

A CRLF ending includes \n, so Ruby can usually split it while reading text. A file containing only CR endings does not contain \n; therefore, a default line reader may treat the whole file as one line.

Ruby's newline: :universal option recognizes LF, CRLF, and CR endings and normalizes each one to \n while reading. This is useful when your program accepts text from unknown platforms or external tools.

Line-oriented processing matters because it lets programs handle large files incrementally instead of loading all content into memory at once.

Mental Model

Think of a newline as the end-of-row marker in a spreadsheet.

Ruby's normal reader looks for one specific marker: \n. Most files use it directly or as part of \r\n, so the rows are found. But a CR-only file uses a different marker, \r, and Ruby does not see its usual boundary.

Universal newline mode is like a translator at the door: it recognizes all common row-ending markers and converts them into the same internal marker before your code receives each line.

Take Quiz

Syntax and Examples

Read a file line by line with universal newline handling:

line_num = 0

File.open("xxx.txt", "r", newline: :universal) do |file|
  file.each_line do |line|
    line_num += 1
    print "#{line_num} #{line}"
  end
end

newline: :universal makes Ruby recognize these endings:

LF:    \n
CRLF:  \r\n
CR:    \r

After conversion, each yielded line ends in \n when the original input had a line ending.

For input redirected through the shell, configure STDIN before iterating:

STDIN.set_encoding(Encoding::UTF_8, nil, newline: :universal)

STDIN.each_line.with_index(1) do |line, line_number|
  print "#{line_number} #{line}"
end

with_index(1) starts numbering at 1, so you do not need a separate counter variable.

If the input encoding is not known, do not blindly assume UTF-8. In that case, choose an encoding required by your program or inspect and validate the input separately.

Step by Step Execution

Consider a CR-only input file whose bytes represent:

apple\rbanana\rcherry\r

Use this code:

File.open("fruit.txt", "r", newline: :universal) do |file|
  file.each_line.with_index(1) do |line, number|
    puts "#{number}: #{line.chomp}"
  end
end

Execution proceeds as follows:

  1. File.open opens fruit.txt for reading.
  2. newline: :universal recognizes each \r as a newline boundary.
  3. Ruby normalizes the newline after apple to \n and yields "apple\n".
  4. with_index(1) supplies number as 1.
  5. chomp removes the trailing normalized newline, and puts prints 1: apple.
  6. The same process repeats for banana and cherry.

The output is:

1: apple
2: banana
3: cherry

Real World Use Cases

  • Command-line filters: Read redirected logs with ruby report.rb < exported-log.txt.
  • CSV-like exports: Process reports produced on Windows, Linux, or legacy systems.
  • Data migration scripts: Import partner files without requiring contributors to manually convert newlines.
  • Log analysis: Count, search, or transform one log entry at a time.
  • ETL jobs: Stream large text inputs line by line to keep memory use predictable.
  • Developer tools: Build formatters and linters that accept piped input from other commands.
Take Quiz

Real Codebase Usage

In real Ruby programs, line readers are commonly wrapped in a method so files and standard input share the same processing path:

def print_numbered_lines(io)
  io.each_line.with_index(1) do |line, number|
    puts "#{number}: #{line.chomp}"
  end
end

A command-line program can then select its source with a guard clause:

if ARGV.empty?
  STDIN.set_encoding(Encoding::UTF_8, nil, newline: :universal)
  print_numbered_lines(STDIN)
else
  File.open(ARGV.first, "r", newline: :universal) do |file|
    print_numbered_lines(file)
  end
end

Useful project patterns include:

  • Stream processing: Use each_line rather than File.read for large inputs.
  • Resource safety: Use the block form of File.open so Ruby closes the file even if an exception occurs.
  • Normalization at boundaries: Convert newline variants as data enters the application, then keep the rest of the code simple.
  • Validation: Reject invalid encodings or malformed records early when the program requires structured input.
  • Separation of concerns: Keep reading logic separate from parsing and business rules.

Common Mistakes

Using an uninitialized counter

This raises an error because line_num has not been assigned:

File.open("xxx.txt").each do |line|
  print "#{line_num += 1} #{line}"
end

Initialize it first, or use with_index:

File.open("xxx.txt", "r", newline: :universal) do |file|
  file.each_line.with_index(1) do |line, number|
    print "#{number} #{line}"
  end
end

Assuming every text file uses \n

A CR-only file may become one long record with default settings. Use newline: :universal when inputs can originate from different systems.

Calling File.read for a large file

This loads the entire file into memory:

contents = File.read("large.log")

Prefer streaming when processing line by line:

File.open("large.log", "r", newline: :universal) do |file|
  file.each_line { |line| process(line) }
end

Removing too much with strip

strip removes leading and trailing whitespace, not just the newline:

name = line.strip

If leading or trailing spaces are meaningful, use chomp instead:

name = line.chomp

Forgetting that the final line may have no newline

A valid text file can end without a final newline. Ruby still yields that final content as a line; chomp simply has nothing to remove.

Comparisons

ApproachWhat it doesBest useLimitation
file.each_lineStreams lines from an IO objectLarge files and ordinary line processingDefault behavior may not split CR-only input
file.eachAlias for line iteration on IOConcise line processingLess explicit than each_line for beginners
File.readlinesReturns all lines in an arraySmall files when you need random access to linesLoads every line into memory
File.readReturns the entire file as one stringSmall whole-file transformationsYou must split it yourself; can use much memory
newline: :universalRecognizes LF, CRLF, and CR while readingCross-platform or unknown text inputApplies to text line handling, not arbitrary binary formats

chomp and strip are also different:

MethodRemoves
chompA trailing record separator/newline
stripLeading and trailing whitespace, including spaces and tabs
Take Quiz

Cheat Sheet

# Read a known file, accepting LF, CRLF, and CR line endings
File.open("input.txt", "r", newline: :universal) do |file|
  file.each_line do |line|
    puts line.chomp
  end
end

# Number lines starting at 1
File.open("input.txt", "r", newline: :universal) do |file|
  file.each_line.with_index(1) do |line, number|
    puts "#{number}: #{line.chomp}"
  end
end

# Read redirected standard input
STDIN.set_encoding(Encoding::UTF_8, nil, newline: :universal)
STDIN.each_line { |line| puts line.chomp }
  • Default line separator: "\n".
  • Universal mode recognizes: \n, \r\n, and \r.
  • Use each_line to stream instead of loading a whole large file.
  • Use the block form of File.open to close files automatically.
  • Use chomp to remove a final newline while preserving other whitespace.
  • The final line can be yielded even when it has no trailing newline.

FAQ

Why does Ruby read my text file as one line?

The file may use CR-only (\r) line endings while Ruby is looking for its default separator, \n. Open the input with newline: :universal.

Does Ruby handle Windows CRLF line endings?

Yes, CRLF contains \n, so line iteration commonly works. Universal newline mode is still useful when your program must also handle CR-only files consistently.

How do I read lines from STDIN in Ruby?

Use STDIN.each_line or simply ARGF.each_line. For unknown newline styles, configure STDIN with newline: :universal before reading.

Should I use each or each_line on a Ruby file?

Both iterate over lines for IO objects. each_line is clearer because it explicitly states that you are processing lines.

What is the difference between chomp and strip?

chomp removes the trailing newline. strip removes whitespace at both ends, which may accidentally change meaningful spaces.

Can I use readlines instead of each_line?

Yes for small files, but readlines stores the complete file's lines in an array. Use each_line for large or streaming input.

Does universal newline mode fix text encoding problems?

No. Newline format and character encoding are separate concerns. Configure or validate the encoding required by your application.

Take Quiz

Mini Project

Description

Build a small command-line line inspector. It accepts either a file path or redirected standard input, handles common newline formats, and prints non-empty lines with their original line numbers. This mirrors utilities used to inspect imports, logs, and exported reports.

Goal

Create a Ruby script that reads cross-platform text input line by line and prints numbered, non-empty records.

Requirements

Use a file path from ARGV when one is provided.

Read from STDIN when no file path is provided.

Accept LF, CRLF, and CR line endings.

Print line numbers starting at 1.

Ignore lines that are empty after removing their newline.

Close opened files automatically.

Take Quiz

Keep learning

Related questions

View more