CrackedRuby logo

CrackedRuby

Input Methods (gets, readline)

Ruby input methods for reading user input from standard input streams, including gets and readline with their options and behaviors.

Core Modules Kernel Module
3.1.4

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