Overview
Ruby provides several methods for reading input from standard input streams, with gets
and readline
being the primary interfaces. These methods read text from STDIN, handle line terminators, and provide options for processing user input in command-line applications and interactive programs.
The gets
method reads a line from the input stream and returns it as a string, including the trailing newline character. The global variable $/
determines the line separator, defaulting to the system's newline character. When gets
encounters end-of-file (EOF), it returns nil
.
# Reading a single line
input = gets
puts "You entered: #{input}"
# User types: Hello World
# => "You entered: Hello World\n"
The readline
method, available through the Readline module, provides advanced line editing capabilities including history management, tab completion, and cursor movement. Unlike gets
, readline
raises an EOFError
when it encounters EOF.
require 'readline'
# Basic readline usage
input = Readline.readline("Enter command: ")
puts "Command: #{input}"
# Provides line editing and history
Ruby also provides STDIN.gets
and $stdin.gets
for explicit standard input reading, along with methods like getc
, readchar
, and read
for different input patterns. The chomp
method commonly pairs with input methods to remove trailing newlines.
# Different input reading approaches
line_with_newline = STDIN.gets
clean_line = STDIN.gets.chomp
single_char = STDIN.getc
Basic Usage
The gets
method serves as the fundamental approach for reading line-based input in Ruby programs. It reads from the current input source until it encounters a line separator, returning the complete string including the separator.
puts "What's your name?"
name = gets.chomp
puts "Hello, #{name}!"
puts "Enter your age:"
age = gets.to_i
puts "You'll be #{age + 1} next year."
The global variable $/
controls the line separator for gets
. By default, it uses the system's newline character, but you can modify it for specialized parsing needs.
# Default behavior
data = gets # Reads until \n
# Custom separator
original_separator = $/
$/ = "END"
block_data = gets # Reads until "END"
$/ = original_separator
The readline
method offers enhanced input capabilities with built-in line editing. Users can navigate the input line with arrow keys, access command history, and benefit from tab completion when configured.
require 'readline'
# Basic readline with prompt
command = Readline.readline("$ ")
# Enable history
Readline::HISTORY.push(command) unless command.empty?
# Retrieve history
previous_commands = Readline::HISTORY.to_a
puts "Recent commands: #{previous_commands.last(3).join(', ')}"
Reading multiple lines requires loops that handle EOF conditions appropriately. The pattern differs between gets
and readline
due to their different EOF behaviors.
# Reading multiple lines with gets
lines = []
while line = gets
break if line.chomp.empty?
lines << line.chomp
end
# Reading multiple lines with readline
require 'readline'
commands = []
begin
while command = Readline.readline("> ")
break if command == "exit"
commands << command
Readline::HISTORY.push(command)
end
rescue EOFError
puts "\nGoodbye!"
end
Input validation often occurs immediately after reading, checking for expected formats or values before proceeding with program logic.
def read_number(prompt)
loop do
print prompt
input = gets.chomp
return input.to_i if input.match?(/^\d+$/)
puts "Please enter a valid number."
end
end
age = read_number("Enter your age: ")
Error Handling & Debugging
Input operations encounter various error conditions that require careful handling. EOF conditions represent the most common scenario, occurring when users press Ctrl+D on Unix systems or Ctrl+Z on Windows, or when input streams close.
The gets
method returns nil
on EOF, making it straightforward to detect and handle end-of-input conditions in loops.
def read_all_lines
lines = []
while line = gets
lines << line.chomp
end
lines
rescue Interrupt
puts "\nOperation cancelled by user"
[]
end
The readline
method raises EOFError
exceptions on EOF, requiring explicit exception handling rather than nil checking.
require 'readline'
def interactive_session
begin
while true
command = Readline.readline("command> ")
break if command.nil? # This shouldn't happen
process_command(command)
Readline::HISTORY.push(command) unless command.empty?
end
rescue EOFError
puts "\nSession ended"
rescue Interrupt
puts "\nInterrupted by user"
retry # Allow continuation after Ctrl+C
end
end
Input validation errors require different handling strategies depending on whether you want to re-prompt users or fail gracefully. Timeout scenarios become relevant when reading input from network streams or when implementing user response timeouts.
require 'timeout'
def read_with_timeout(prompt, seconds = 10)
print prompt
Timeout::timeout(seconds) do
gets.chomp
end
rescue Timeout::Error
puts "\nTimeout - no input received within #{seconds} seconds"
nil
rescue Interrupt
puts "\nOperation cancelled"
nil
end
# Usage with fallback
response = read_with_timeout("Continue? (y/n): ", 5) || "n"
Encoding issues arise when input contains characters outside the expected encoding. Ruby handles this through encoding conversion and validation.
def safe_input_read
input = gets
return nil if input.nil?
# Ensure valid encoding
if input.valid_encoding?
input.chomp
else
input.encode('UTF-8', invalid: :replace, undef: :replace).chomp
end
rescue Encoding::InvalidByteSequenceError
puts "Invalid character encoding detected"
""
end
Debugging input issues often requires examining the actual bytes received, especially when dealing with different line endings or special characters.
def debug_input
puts "Enter some text (press Ctrl+D to end):"
while line = gets
puts "Raw bytes: #{line.bytes.inspect}"
puts "String: #{line.inspect}"
puts "Chomp: #{line.chomp.inspect}"
puts "---"
end
end
Signal handling becomes important in interactive applications where users might send interrupt signals during input operations.
def robust_input_reader
old_handler = Signal.trap("INT") do
puts "\nUse 'quit' to exit properly"
throw :continue
end
catch :continue do
while true
print "Enter command (or 'quit'): "
command = gets.chomp
break if command == "quit"
process_command(command)
end
end
ensure
Signal.trap("INT", old_handler) if old_handler
end
Testing Strategies
Testing input methods requires redirecting standard input to provide predictable data during test execution. Ruby's StringIO class serves as the primary tool for simulating user input in tests.
require 'stringio'
require 'minitest/autorun'
class InputReaderTest < Minitest::Test
def test_gets_reading
simulated_input = StringIO.new("test input\n")
original_stdin = $stdin
$stdin = simulated_input
result = gets.chomp
assert_equal "test input", result
ensure
$stdin = original_stdin
end
def test_multiple_line_reading
input_data = "line1\nline2\nline3\n"
simulated_input = StringIO.new(input_data)
original_stdin = $stdin
$stdin = simulated_input
lines = []
while line = gets
lines << line.chomp
end
assert_equal ["line1", "line2", "line3"], lines
ensure
$stdin = original_stdin
end
end
Mocking readline requires more sophisticated approaches since it involves external library components and history management.
require 'minitest/autorun'
require 'readline'
class ReadlineTest < Minitest::Test
def setup
@original_history = Readline::HISTORY.to_a
Readline::HISTORY.clear
end
def teardown
Readline::HISTORY.clear
@original_history.each { |item| Readline::HISTORY.push(item) }
end
def test_readline_with_mock
# Mock readline method
Readline.define_singleton_method(:readline) do |prompt|
@last_prompt = prompt
"mocked input"
end
result = Readline.readline("Test prompt: ")
assert_equal "mocked input", result
assert_equal "Test prompt: ", @last_prompt
end
end
Testing EOF conditions requires simulating end-of-file scenarios appropriately for both gets
and readline
methods.
class EOFTest < Minitest::Test
def test_gets_eof_handling
simulated_input = StringIO.new("") # Empty input simulates EOF
original_stdin = $stdin
$stdin = simulated_input
result = gets
assert_nil result
ensure
$stdin = original_stdin
end
def test_readline_eof_handling
# Simulate EOFError for readline
Readline.define_singleton_method(:readline) do |prompt|
raise EOFError
end
assert_raises(EOFError) do
Readline.readline("Prompt: ")
end
end
end
Integration testing often requires spawning subprocesses to test actual input/output behavior in realistic scenarios.
require 'open3'
class IntegrationTest < Minitest::Test
def test_interactive_program
script = <<~RUBY
puts "Enter your name:"
name = gets.chomp
puts "Hello, \#{name}!"
RUBY
# Write script to temporary file
script_file = "/tmp/test_script.rb"
File.write(script_file, script)
# Run with input
output, error, status = Open3.capture3(
"ruby #{script_file}",
stdin_data: "Alice\n"
)
assert_includes output, "Enter your name:"
assert_includes output, "Hello, Alice!"
assert status.success?
ensure
File.delete(script_file) if File.exist?(script_file)
end
end
Helper methods streamline input testing by encapsulating the stdin redirection pattern.
module InputTestHelpers
def with_simulated_input(input_string)
original_stdin = $stdin
$stdin = StringIO.new(input_string)
yield
ensure
$stdin = original_stdin
end
end
class MyTest < Minitest::Test
include InputTestHelpers
def test_with_helper
with_simulated_input("test\ndata\n") do
first_line = gets.chomp
second_line = gets.chomp
assert_equal "test", first_line
assert_equal "data", second_line
end
end
end
Common Pitfalls
The gets
method includes the trailing newline character in the returned string, which often surprises developers who expect clean input. This leads to comparison failures and unexpected string content.
# Problematic code
puts "Enter 'yes' to continue:"
answer = gets
if answer == "yes" # This fails!
puts "Continuing..."
else
puts "Not continuing"
end
# Correct approach
puts "Enter 'yes' to continue:"
answer = gets.chomp
if answer == "yes" # This works
puts "Continuing..."
else
puts "Not continuing"
end
Global variable side effects occur because gets
sets $_
to the last read line and updates $.
with the current line number. These side effects can interfere with other parts of the program.
# Demonstration of side effects
puts "Enter first line:"
first = gets
puts "$_ contains: #{$_.inspect}" # Contains the input
puts "$. is: #{$.inspect}" # Shows line number
# Later code might unexpectedly depend on these variables
def process_data
# This method assumes $_ contains specific data
puts "Processing: #{$_}"
end
The line separator variable $/
affects all gets
operations globally. Modifying it in one part of the program impacts input reading everywhere, leading to difficult-to-trace bugs.
# Dangerous global modification
def read_custom_format
original_separator = $/
$/ = "|||" # Custom separator
data = gets
# Forgot to restore - breaks all subsequent gets calls!
data
end
# Correct approach
def read_custom_format
original_separator = $/
begin
$/ = "|||"
gets
ensure
$/ = original_separator # Always restore
end
end
Type conversion issues arise when developers assume input will always be valid. The to_i
and to_f
methods silently convert invalid input to zero, masking validation problems.
# Problematic input handling
puts "Enter your age:"
age = gets.to_i # "abc" becomes 0, "25abc" becomes 25
# Better validation approach
def read_integer(prompt)
loop do
print prompt
input = gets.chomp
if input.match?(/^\d+$/)
return input.to_i
else
puts "Please enter a valid integer."
end
end
end
EOF handling differs significantly between gets
and readline
. Using patterns appropriate for one method with the other leads to incorrect behavior or unhandled exceptions.
# Wrong pattern for readline
require 'readline'
while command = Readline.readline("> ") # This raises EOFError!
process_command(command)
end
# Correct readline pattern
require 'readline'
begin
while command = Readline.readline("> ")
process_command(command)
end
rescue EOFError
puts "Session ended"
end
Buffer mixing occurs when combining gets
, getc
, and other input methods. Ruby's input buffering can cause characters to be consumed in unexpected order.
# Problematic mixing
puts "Press any key..."
key = getc
puts "Enter your name:"
name = gets.chomp # Might miss characters due to buffering
# Better approach - consistent input method
puts "Press Enter to continue..."
gets
puts "Enter your name:"
name = gets.chomp
Readline history pollution happens when applications don't manage history appropriately, leading to cluttered or inappropriate command history.
# Problematic history management
require 'readline'
password = Readline.readline("Password: ")
Readline::HISTORY.push(password) # Security risk!
# Better approach
require 'readline'
# Don't add sensitive input to history
password = Readline.readline("Password: ")
# History management for regular commands only
if regular_command?(input)
Readline::HISTORY.push(input)
end
Platform-specific newline handling can cause issues when applications move between systems with different line ending conventions.
# Platform-aware input handling
def normalize_line_endings(text)
text.gsub(/\r\n?/, "\n")
end
def cross_platform_gets
input = gets
return nil if input.nil?
normalize_line_endings(input).chomp
end
Production Patterns
Command-line interface applications rely heavily on input methods for user interaction. Robust CLI tools implement comprehensive input handling with validation, help systems, and graceful error recovery.
class CLIApplication
def initialize
@commands = {
'help' => method(:show_help),
'quit' => method(:quit_application),
'status' => method(:show_status)
}
@running = true
end
def run
puts "Application started. Type 'help' for commands."
while @running
begin
command_line = Readline.readline("app> ")
next if command_line.nil? || command_line.empty?
Readline::HISTORY.push(command_line)
command, *args = command_line.split
if @commands.key?(command)
@commands[command].call(*args)
else
puts "Unknown command: #{command}. Type 'help' for available commands."
end
rescue EOFError
puts "\nExiting..."
break
rescue Interrupt
puts "\nInterrupted. Type 'quit' to exit."
rescue StandardError => e
puts "Error: #{e.message}"
end
end
end
private
def show_help(*args)
puts "Available commands:"
@commands.keys.each { |cmd| puts " #{cmd}" }
end
def quit_application(*args)
@running = false
puts "Goodbye!"
end
def show_status(*args)
puts "Application running normally"
end
end
Web application deployment often requires handling input in environments where STDIN might not be available or behave differently. Applications detect these conditions and adapt their input strategies accordingly.
class InputManager
def self.production_safe_input(prompt, default = nil)
return default unless STDIN.tty?
print prompt
input = STDIN.gets
return default if input.nil?
cleaned = input.chomp
cleaned.empty? ? default : cleaned
rescue StandardError
default
end
def self.batch_mode?
!STDIN.tty? || ENV['BATCH_MODE'] == 'true'
end
def self.interactive_confirmation(message, default_yes = false)
return default_yes if batch_mode?
loop do
prompt = default_yes ? "#{message} (Y/n): " : "#{message} (y/N): "
response = production_safe_input(prompt, "").downcase
case response
when 'y', 'yes'
return true
when 'n', 'no'
return false
when ''
return default_yes
else
puts "Please answer 'y' or 'n'"
end
end
end
end
Configuration and setup scripts use input methods for collecting deployment parameters and user preferences. These scripts handle various input types and provide sensible defaults.
class ConfigurationSetup
def self.run
config = {}
puts "Setting up application configuration..."
config[:database_url] = prompt_with_validation(
"Database URL: ",
validator: ->(input) { input.match?(/^postgres:\/\//) },
error_message: "Please provide a valid PostgreSQL URL"
)
config[:redis_url] = prompt_with_default(
"Redis URL",
"redis://localhost:6379"
)
config[:worker_count] = prompt_integer(
"Number of workers",
default: 4,
min: 1,
max: 32
)
config[:log_level] = prompt_choice(
"Log level",
choices: %w[DEBUG INFO WARN ERROR],
default: 'INFO'
)
save_configuration(config)
puts "Configuration saved successfully!"
end
private
def self.prompt_with_validation(prompt, validator:, error_message:)
loop do
print prompt
input = gets.chomp
return input if validator.call(input)
puts error_message
end
end
def self.prompt_with_default(label, default)
print "#{label} (#{default}): "
input = gets.chomp
input.empty? ? default : input
end
def self.prompt_integer(label, default:, min: nil, max: nil)
loop do
print "#{label} (#{default}): "
input = gets.chomp
if input.empty?
return default
elsif input.match?(/^\d+$/)
value = input.to_i
if (min.nil? || value >= min) && (max.nil? || value <= max)
return value
else
puts "Value must be between #{min} and #{max}"
end
else
puts "Please enter a valid integer"
end
end
end
def self.prompt_choice(label, choices:, default:)
loop do
puts "#{label} (#{choices.join('/')}) [#{default}]: "
input = gets.chomp.upcase
return default if input.empty?
return input if choices.include?(input)
puts "Please choose from: #{choices.join(', ')}"
end
end
def self.save_configuration(config)
File.write('.env', config.map { |k, v| "#{k.to_s.upcase}=#{v}" }.join("\n"))
end
end
Monitoring and logging input operations helps track user behavior and diagnose issues in production environments. Applications implement input auditing while respecting privacy concerns.
class AuditedInputReader
def initialize(logger = Logger.new(STDOUT))
@logger = logger
@session_id = SecureRandom.hex(8)
end
def read_command(prompt, sensitive: false)
start_time = Time.now
begin
@logger.info("INPUT_START", {
session: @session_id,
prompt: sensitive ? "[REDACTED]" : prompt,
timestamp: start_time.iso8601
})
result = Readline.readline(prompt)
duration = Time.now - start_time
@logger.info("INPUT_SUCCESS", {
session: @session_id,
length: result&.length || 0,
duration: duration.round(3),
eof: result.nil?
})
result
rescue EOFError
@logger.info("INPUT_EOF", { session: @session_id })
raise
rescue Interrupt
@logger.warn("INPUT_INTERRUPT", { session: @session_id })
raise
rescue StandardError => e
@logger.error("INPUT_ERROR", {
session: @session_id,
error: e.class.name,
message: e.message
})
raise
end
end
end
Reference
Core Input Methods
Method | Parameters | Returns | Description |
---|---|---|---|
gets |
sep = $/ |
String or nil |
Reads line from STDIN until separator |
gets(limit) |
limit (Integer) |
String or nil |
Reads up to limit characters |
gets(sep, limit) |
sep (String), limit (Integer) |
String or nil |
Reads until separator or limit |
STDIN.gets |
sep = $/ , limit = nil |
String or nil |
Explicit STDIN reading |
getc |
None | String or nil |
Reads single character |
readchar |
None | String |
Reads single character, raises EOFError |
readline |
limit = nil |
String |
Reads line, raises EOFError on EOF |
Readline Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Readline.readline |
prompt = "" , add_hist = false |
String or nil |
Enhanced line input with editing |
Readline.input= |
input (IO) |
IO |
Sets input stream |
Readline.output= |
output (IO) |
IO |
Sets output stream |
Readline.completion_proc= |
proc (Proc) |
Proc |
Sets tab completion handler |
Readline.completion_append_character= |
char (String) |
String |
Sets character appended to completions |
Readline.vi_editing_mode |
None | nil |
Enables vi-style line editing |
Readline.emacs_editing_mode |
None | nil |
Enables emacs-style line editing |
History Management
Method | Parameters | Returns | Description |
---|---|---|---|
Readline::HISTORY.push |
string (String) |
Array |
Adds string to history |
Readline::HISTORY.pop |
None | String or nil |
Removes and returns last entry |
Readline::HISTORY.clear |
None | Array |
Removes all history entries |
Readline::HISTORY.length |
None | Integer |
Returns history size |
Readline::HISTORY.to_a |
None | Array |
Returns history as array |
Readline::HISTORY[index] |
index (Integer) |
String or nil |
Accesses history entry by index |
Readline::HISTORY.delete_at |
index (Integer) |
String or nil |
Removes entry at index |
Global Variables
Variable | Type | Description |
---|---|---|
$/ |
String |
Input record separator for gets |
$_ |
String |
Last string read by gets |
$. |
Integer |
Current input line number |
$stdin |
IO |
Standard input stream object |
Common Options and Constants
Option | Value | Description |
---|---|---|
Default separator | "\n" |
System newline character |
EOF return (gets) | nil |
Indicates end of file |
EOF behavior (readline) | EOFError |
Exception raised on end of file |
Default prompt | "" |
Empty string prompt |
History size limit | Platform dependent | Maximum history entries |
Error Types
Exception | Trigger | Description |
---|---|---|
EOFError |
readline at EOF |
End of file reached |
Interrupt |
Ctrl+C during input | User interrupt signal |
Errno::EIO |
Terminal disconnection | Input/output error |
Encoding::InvalidByteSequenceError |
Invalid character encoding | Encoding conversion failure |
ArgumentError |
Invalid separator or limit | Method parameter error |
Decision Table: Method Selection
Use Case | Method | Reason |
---|---|---|
Simple line input | gets.chomp |
Straightforward, handles EOF |
Interactive CLI | Readline.readline |
Line editing, history support |
Single character | getc |
No line buffering |
Must handle EOF as error | readchar |
Raises exception on EOF |
Custom separator | gets(separator) |
Flexible line definition |
Limited input length | gets(limit) |
Prevents excessive memory use |
Batch processing | STDIN.gets |
Explicit about input source |
Password input | Custom implementation | Security considerations |
Compatibility Notes
Ruby Version | Feature | Notes |
---|---|---|
1.9+ | Encoding aware | Input respects source encoding |
2.0+ | Refined EOF handling | Consistent nil/exception behavior |
2.1+ | Readline improvements | Better Windows support |
2.4+ | IO timeout support | Timeout module integration |
3.0+ | Fiber scheduler | Async input operations |