CrackedRuby logo

CrackedRuby

STDIN, STDOUT, STDERR

Standard input, output, and error stream handling in Ruby through global constants and IO operations.

Core Built-in Classes File and IO Classes
2.9.5

Overview

Ruby provides three global constants for standard I/O streams: STDIN, STDOUT, and STDERR. These constants reference IO objects that connect Ruby programs to the operating system's standard input, output, and error streams respectively.

STDIN represents the standard input stream, typically connected to keyboard input or piped data. STDOUT handles standard output, normally displayed in the terminal or redirected to files. STDERR manages error output, allowing programs to separate error messages from regular output for better shell scripting and debugging.

Each constant is an instance of the IO class, providing methods for reading from input streams and writing to output streams. Ruby also provides global methods like puts, print, and gets that operate on these streams by default.

# Check the classes
STDIN.class  # => IO
STDOUT.class # => IO  
STDERR.class # => IO

# Default stream file descriptors
STDIN.fileno  # => 0
STDOUT.fileno # => 1
STDERR.fileno # => 2

The streams handle encoding conversion automatically based on the system locale and can be configured for different character encodings. They also support various modes for reading and writing, including line-buffered and block-buffered operations.

# Stream encodings
STDOUT.external_encoding # => #<Encoding:UTF-8>
STDIN.external_encoding  # => #<Encoding:UTF-8>

# Stream sync status
STDOUT.sync # => false (buffered by default)
STDERR.sync # => true (unbuffered for immediate error display)

Basic Usage

Reading from STDIN uses methods like gets, read, and readlines. The global gets method reads from STDIN by default, while STDIN.gets explicitly reads from the input stream.

# Reading a single line
puts "Enter your name:"
name = gets.chomp
puts "Hello, #{name}!"

# Reading with explicit STDIN
puts "Enter age:"  
age = STDIN.gets.to_i

Writing to STDOUT and STDERR uses methods like puts, print, and write. Global output methods default to STDOUT, while STDERR requires explicit targeting for error messages.

# Standard output
puts "Processing data..."        # Goes to STDOUT
STDOUT.puts "Explicit output"    # Same as above

# Error output  
STDERR.puts "Error: File not found"
warn "Warning message"           # Shorthand for STDERR output

The streams support different output formatting methods. puts adds newlines automatically, print outputs without newlines, and write returns the number of bytes written.

# Different output methods
puts "Line 1", "Line 2"     # Two lines with newlines
print "No newline"          # No automatic newline
STDOUT.write "Raw bytes\n"  # Returns byte count

# Formatting output
printf "Name: %s, Age: %d\n", "Alice", 30
STDOUT.printf "Formatted: %.2f\n", 3.14159

File descriptor operations allow low-level stream manipulation. Each stream has a numeric file descriptor that the operating system uses for I/O operations.

# File descriptor operations
STDOUT.flush                    # Force output buffer flush
STDERR.sync = false            # Enable buffering for STDERR
STDIN.close_on_exec = true     # Close on exec() calls

# Stream status checks
STDIN.closed?    # => false
STDOUT.tty?      # => true (if connected to terminal)
STDERR.isatty    # => true (alias for tty?)

Error Handling & Debugging

I/O operations can raise various exceptions that require proper handling. EOFError occurs when attempting to read past the end of input, while IOError handles general I/O failures.

# Handling end-of-file conditions
begin
  loop do
    line = STDIN.readline  # Raises EOFError at EOF
    puts "Read: #{line}"
  end
rescue EOFError
  puts "End of input reached"
end

# Safe reading with gets (returns nil at EOF)
while line = STDIN.gets
  puts "Processing: #{line.chomp}"
end

Stream redirection and pipe failures require careful error handling. When STDOUT or STDERR streams are redirected to files or pipes, write operations can fail if the destination becomes unavailable.

# Handling write failures
begin
  1000.times do |i|
    STDOUT.puts "Line #{i}"
    STDOUT.flush  # Force write to detect pipe breaks
  end
rescue Errno::EPIPE
  # Pipe broken (e.g., consumer process terminated)
  STDERR.puts "Output pipe closed"
  exit 1
rescue Errno::ENOSPC
  # No space left on device
  STDERR.puts "Disk full"
  exit 1
end

Encoding errors occur when input data doesn't match the expected character encoding. Ruby provides several strategies for handling invalid byte sequences.

# Handling encoding errors
STDIN.set_encoding('UTF-8', invalid: :replace, undef: :replace)

begin
  content = STDIN.read
  puts content
rescue Encoding::InvalidByteSequenceError => e
  STDERR.puts "Encoding error: #{e.message}"
  # Use binary mode for problematic input
  STDIN.binmode
  content = STDIN.read
end

Debugging I/O operations often requires examining stream states and buffer contents. Ruby provides methods to inspect stream configuration and status.

# Stream debugging information
def debug_stream_info(stream, name)
  puts "#{name} info:"
  puts "  Encoding: #{stream.external_encoding}"
  puts "  Internal: #{stream.internal_encoding}"
  puts "  Sync: #{stream.sync}"
  puts "  Closed: #{stream.closed?}"
  puts "  TTY: #{stream.tty?}" if stream.respond_to?(:tty?)
  puts "  FD: #{stream.fileno}"
  puts
end

debug_stream_info(STDIN, "STDIN")
debug_stream_info(STDOUT, "STDOUT")  
debug_stream_info(STDERR, "STDERR")

Production Patterns

Command-line applications use standard streams for structured input and output processing. Separating data output on STDOUT from status messages on STDERR enables proper shell pipeline integration.

# Command-line data processor
class DataProcessor
  def self.process_input
    line_count = 0
    error_count = 0
    
    STDERR.puts "Starting data processing..."
    
    while line = STDIN.gets
      begin
        processed = transform_data(line.chomp)
        STDOUT.puts processed
        line_count += 1
        
        # Progress reporting to STDERR
        STDERR.puts "Processed #{line_count} lines" if line_count % 1000 == 0
        
      rescue StandardError => e
        STDERR.puts "Error on line #{line_count + 1}: #{e.message}"
        error_count += 1
      end
    end
    
    # Final summary to STDERR
    STDERR.puts "Completed: #{line_count} lines, #{error_count} errors"
    exit(error_count > 0 ? 1 : 0)
  end
  
  private
  
  def self.transform_data(line)
    # Data transformation logic
    line.upcase.reverse
  end
end

Web applications and background services require careful stream handling for logging and output capture. Production environments often redirect streams to log files or monitoring systems.

# Production logging setup
class ProductionLogger
  def initialize
    # Ensure unbuffered error output
    STDERR.sync = true
    
    # Set up structured logging
    setup_log_rotation
  end
  
  def log_info(message)
    timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
    STDOUT.puts "[INFO] #{timestamp}: #{message}"
    STDOUT.flush
  end
  
  def log_error(message, error = nil)
    timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
    STDERR.puts "[ERROR] #{timestamp}: #{message}"
    
    if error
      STDERR.puts "Exception: #{error.class}: #{error.message}"
      STDERR.puts error.backtrace.join("\n") if error.backtrace
    end
    
    STDERR.flush
  end
  
  private
  
  def setup_log_rotation
    # Handle SIGUSUR1 for log rotation
    Signal.trap('USR1') do
      STDOUT.reopen('/var/log/app/output.log', 'a')
      STDERR.reopen('/var/log/app/error.log', 'a')
    end
  end
end

Docker and container environments require special stream handling considerations. Container orchestrators expect applications to write logs to STDOUT and STDERR for collection by logging drivers.

# Container-friendly application
class ContainerApp
  def self.run
    # JSON structured logging for container log drivers
    logger = ContainerLogger.new
    
    Signal.trap('TERM') do
      logger.log(:info, "Received SIGTERM, shutting down gracefully")
      shutdown_gracefully
      exit 0
    end
    
    begin
      main_application_loop(logger)
    rescue StandardError => e
      logger.log(:error, "Application crashed", error: e)
      exit 1
    end
  end
  
  private
  
  def self.main_application_loop(logger)
    # Main application logic with structured logging
    loop do
      begin
        process_request(logger)
      rescue StandardError => e
        logger.log(:error, "Request processing failed", error: e)
      end
    end
  end
end

class ContainerLogger
  def log(level, message, **metadata)
    log_entry = {
      timestamp: Time.now.utc.iso8601,
      level: level.to_s.upcase,
      message: message
    }.merge(metadata)
    
    output_stream = level == :error ? STDERR : STDOUT
    output_stream.puts JSON.generate(log_entry)
    output_stream.flush
  end
end

Common Pitfalls

Buffer flushing represents one of the most common sources of confusion. STDOUT is typically line-buffered when connected to a terminal but fully buffered when redirected to files or pipes, causing unexpected delays in output appearance.

# Problem: Output appears delayed or missing
puts "Processing..."
expensive_operation  # Output may not appear immediately

# Solution: Explicit flushing
puts "Processing..."
STDOUT.flush         # Force immediate output
expensive_operation

# Alternative: Disable buffering entirely
STDOUT.sync = true
puts "This appears immediately"

Encoding mismatches between input data and stream configurations cause silent data corruption or encoding errors. Default encodings depend on system locale settings, creating portability issues.

# Problem: Encoding assumption failures
# Input might be Latin-1 but STDIN expects UTF-8
data = STDIN.read  # May raise Encoding::InvalidByteSequenceError

# Solution: Explicit encoding handling
STDIN.set_encoding('BINARY')
binary_data = STDIN.read
utf8_data = binary_data.force_encoding('UTF-8')

# Or detect and handle encoding issues
STDIN.set_encoding('UTF-8:UTF-8', invalid: :replace, undef: :replace)
clean_data = STDIN.read

Global method confusion leads to unexpected behavior when methods like puts and gets don't operate on the expected streams. The warn method writes to STDERR, not STDOUT, catching developers off-guard.

# Confusion: Assuming warn goes to STDOUT
warn "This goes to STDERR, not STDOUT!"

# Explicit stream targeting
STDOUT.puts "This definitely goes to STDOUT"
STDERR.puts "This definitely goes to STDERR"

# Getting input from wrong stream
# Problem: Using gets when STDIN might be redirected
input = gets  # Might read from file instead of terminal

# Solution: Explicit stream or terminal detection
if STDIN.tty?
  input = STDIN.gets
else
  input = IO.console.gets  # Always read from terminal
end

Stream redirection surprises occur when programs assume streams connect to terminals. Applications may behave differently when STDOUT is piped or redirected, especially regarding interactive prompts and formatting.

# Problem: Interactive prompts in non-interactive environments
print "Enter password: "
password = gets.chomp  # Hangs if STDIN redirected from file

# Solution: Check for terminal interaction
def get_user_input(prompt)
  if STDIN.tty? && STDOUT.tty?
    print prompt
    STDOUT.flush
    STDIN.gets.chomp
  else
    # Non-interactive mode
    ENV['DEFAULT_INPUT'] || raise("No input available in non-interactive mode")
  end
end

# Problem: ANSI colors in redirected output
puts "\e[31mError message\e[0m"  # Color codes appear in files

# Solution: Conditional formatting
def colorize(text, color_code)
  if STDOUT.tty?
    "\e[#{color_code}m#{text}\e[0m"
  else
    text
  end
end

puts colorize("Error message", 31)

Signal handling complications arise when programs don't properly handle stream closure during signal processing. Interrupted I/O operations can leave streams in inconsistent states.

# Problem: Interrupted I/O operations
Signal.trap('INT') do
  puts "Interrupted!"  # Dangerous in signal handler
  exit
end

# Solution: Defer stream operations outside signal handlers
interrupted = false

Signal.trap('INT') do
  interrupted = true
end

while line = STDIN.gets
  break if interrupted
  
  begin
    process_line(line)
  rescue StandardError => e
    STDERR.puts "Error: #{e.message}"
  end
end

if interrupted
  STDERR.puts "Processing interrupted by user"
  exit 130  # Standard exit code for SIGINT
end

Reference

Global Constants

Constant Type File Descriptor Default Sync Description
STDIN IO 0 N/A (input) Standard input stream
STDOUT IO 1 false Standard output stream
STDERR IO 2 true Standard error stream

Core IO Methods

Method Parameters Returns Description
#gets(separator, limit) separator (String), limit (Integer) String or nil Read line from stream
#read(length, buffer) length (Integer), buffer (String) String or nil Read bytes from stream
#readline(separator, limit) separator (String), limit (Integer) String Read line, raise EOFError at EOF
#readlines(separator, limit) separator (String), limit (Integer) Array<String> Read all lines into array
#puts(*objects) Variable arguments nil Write objects with newlines
#print(*objects) Variable arguments nil Write objects without newlines
#write(string) string (String) Integer Write string, return bytes written
#printf(format, *args) format (String), arguments nil Write formatted string

Stream Control Methods

Method Parameters Returns Description
#flush None IO Flush output buffers
#sync None Boolean Get synchronous mode status
#sync=(boolean) boolean (Boolean) Boolean Set synchronous mode
#close None nil Close stream
#closed? None Boolean Check if stream is closed
#eof? None Boolean Check for end-of-file
#tty? None Boolean Check if stream is terminal
#isatty None Boolean Alias for tty?

Encoding Methods

Method Parameters Returns Description
#external_encoding None Encoding Get external encoding
#internal_encoding None Encoding or nil Get internal encoding
#set_encoding(encoding, **options) encoding (String/Encoding), options IO Set stream encoding
#binmode None IO Set binary mode

Global Helper Methods

Method Parameters Returns Description
gets(separator, limit) separator (String), limit (Integer) String or nil Read from STDIN
puts(*objects) Variable arguments nil Write to STDOUT
print(*objects) Variable arguments nil Write to STDOUT without newlines
printf(format, *args) format (String), arguments nil Formatted write to STDOUT
warn(*messages, **options) messages (Array), options nil Write to STDERR

Common Exceptions

Exception Description Common Causes
EOFError End of file reached Reading past input end with readline
IOError General I/O error Closed streams, invalid operations
Errno::EPIPE Broken pipe Writing to closed pipe/redirect
Errno::ENOSPC No space left Disk full during write
Encoding::InvalidByteSequenceError Invalid byte sequence Encoding mismatches
Encoding::UndefinedConversionError Character conversion failed Unsupported character conversion

Stream Configuration Options

Option Values Default Description
sync true, false STDOUT: false, STDERR: true Synchronous I/O mode
encoding Encoding name/object System default Character encoding
invalid :replace, :ignore Exception Invalid byte handling
undef :replace, :ignore Exception Undefined character handling
replace String "?" Replacement character

File Descriptor Constants

Constant Value Standard Name Description
STDIN.fileno 0 stdin Standard input
STDOUT.fileno 1 stdout Standard output
STDERR.fileno 2 stderr Standard error