rubyguides

Ruby Scripting Basics for Automation

Ruby is an excellent choice for scripting and automation. Its clean syntax, deep standard library, and friendly OS integration make it well suited to automating repetitive tasks, processing files, and building command-line tools. This tutorial covers the fundamentals of Ruby scripting basics for automation workflows.

We’ll start by writing a one-file script with a shebang and command-line arguments, then move to reading and writing files with the standard File and IO classes, shelling out to other programs through backticks and Open3, and parsing options with OptionParser. The goal is not to replace Bash everywhere, but to give you a comfortable upgrade path when a shell script grows past its happy size and you would rather work in Ruby for the rest. For related reading, see the Ruby OptionParser CLI guide and the array tutorial which covers data-shape patterns you’ll lean on inside scripts.

Intro Context

Ruby scripts tend to age better than ad hoc shell snippets because the syntax stays readable as the script grows. You can start with a single file, then keep adding small helper methods, structured data, and error handling without changing languages halfway through the job.

That flexibility makes Ruby a good fit for both quick one-off tasks and repeatable automation. If you need to rename files, normalize data, talk to an API, or orchestrate a few command-line tools, Ruby gives you a single place to write the logic and test it with ordinary code.

Why Learn Scripting

The fastest way to learn scripting in Ruby is to focus on one repetitive task and automate only that task first. Once you see how the file, command, and option examples fit together, it becomes easier to decide when a shell script is enough and when Ruby gives you a cleaner path forward. That judgment is useful because it keeps the script small while still leaving room to grow.

Ruby also gives you a nicer bridge between quick one-off commands and longer-lived automation. You can start with a simple file that runs from the terminal, then add arguments, configuration, and error handling when the script starts to earn its keep. This tutorial follows that progression so the examples feel like one workflow instead of a pile of unrelated tricks.

Why use Ruby for scripting?

Ruby was designed for programmer happiness, and that shows in scripting tasks. Here’s why Ruby works well for automation:

  • Readable syntax — Ruby reads almost like English, making scripts easy to understand and maintain
  • Rich standard library — Everything from file handling to HTTP requests is built-in
  • Cross-platform — Ruby runs on Linux, macOS, and Windows with the same code
  • Great ecosystem — Tools like Rake, Thor, and Bundler make complex scripts manageable

Running your first script

Create a file called hello.rb with this content:

#!/usr/bin/env ruby
# A simple Ruby script

puts "Hello, Automation World!"

Run it with the ruby command. Once the file is executable after running chmod +x hello.rb, the shebang line lets you invoke it directly with ./hello.rb without typing ruby each time:

ruby hello.rb

The #!/usr/bin/env ruby shebang line lets you run the script directly (./hello.rb) on Unix systems.

If you are used to shell scripts, the biggest shift is that Ruby keeps the logic in normal methods instead of spreading it across pipes and subshells. That usually makes the script easier to extend once the task stops being trivial.

Working with files

One of the most common automation tasks is processing files. Ruby makes this straightforward.

Reading files

Ruby gives you several ways to read a file. File.read loads the entire contents into memory, which is fine for configuration files and small data sets. For larger files, File.foreach streams one line at a time so the memory footprint stays constant regardless of file size:

# Read entire file into memory
content = File.read('data.txt')
puts content

# Read file line by line (memory efficient for large files)
File.foreach('data.txt') do |line|
  puts line unless line.strip.empty?
end

Writing files

Writing follows a similar pattern. File.write is the simplest option when you have the full content ready; it overwrites the file if it exists. For appending or for building output line by line, File.open with the 'a' mode gives you more control over how the data lands in the file:

# Write to a new file (overwrites existing content)
File.write('output.txt', 'Hello, World!')

# Append to existing file
File.open('log.txt', 'a') do |file|
  file.puts "#{Time.now} - Script completed"
end

Processing CSV files

Ruby’s CSV library makes data processing simple:

require 'csv'

CSV.foreach('data.csv', headers: true) do |row|
  name = row['name']
  email = row['email']
puts "#{name}: #{email}"
end

One practical pattern is to normalize each row as soon as you read it. That keeps the rest of the script focused on the action you want to take instead of on the details of string parsing.

Running system commands

Ruby can execute shell commands and capture their output. Backticks are the quickest way to grab a command’s stdout, while system gives you control over whether output goes to the terminal and lets you check the exit status afterward:

Running commands

# Run a command and get output
output = `ls -la`
puts output

# Or use the system method for more control
system('ls', '-la')

# Capture exit status
if $?.success?
  puts "Command succeeded"
else
  puts "Command failed with code: #{$?.exitcode}"
end

Using the open3 library

For more control over command execution:

require 'open3'

stdout, stderr, status = Open3.capture3('ls', '-la')

if status.success?
  puts stdout
else
puts "Error: #{stderr}"
end

The command helpers let you choose between convenience and control. Backticks are quick for simple capture, system is good for fire-and-forget execution, and Open3 gives you the most detail when you need to inspect stdout, stderr, and exit status separately.

Command-line arguments

Most automation scripts need to accept arguments:

# Access command-line arguments
filename = ARGV[0]
puts "Processing: #{filename}"

# Handle multiple arguments
ARGV.each_with_index do |arg, index|
  puts "Argument #{index + 1}: #{arg}"
end

# Use options with optparse for complex CLI tools
require 'optparse'

options = { verbose: false, count: 1 }

OptionParser.new do |parser|
  parser.on('-v', '--verbose', 'Increase verbosity') do
    options[:verbose] = true
  end
  parser.on('-c N', '--count=N', Integer, 'Repeat N times') do |n|
    options[:count] = n
  end
end.parse!

puts "Running #{options[:count]} times" if options[:verbose]

Option parsing is usually the point where a script starts feeling like a reusable tool. Once the script accepts named options, it becomes much easier to run safely in cron, from another script, or by a teammate who was not part of the original setup.

Automating repetitive tasks

Here’s a practical example: processing all files in a directory. The script below uses Dir.glob to find matching files, checks each file’s modification time with File.mtime, transforms the content, and writes the result to a new file. The next guard clause skips files that were already processed today, which makes the script safe to run repeatedly:

#!/usr/bin/env ruby
require 'fileutils'

# Process all .txt files in current directory
Dir.glob('*.txt').each do |filename|
  # Skip if file was modified today
  next if File.mtime(filename) > Time.now - 86400
  
  # Read and transform content
  content = File.read(filename)
  transformed = content.upcase
  
  # Write to new file
  new_filename = filename.sub('.txt', '_uppercase.txt')
  File.write(new_filename, transformed)
  
  puts "Processed: #{filename} -> #{new_filename}"
end

Working with JSON and YAML

Modern automation often involves configuration files:

require 'json'
require 'yaml'

# Read JSON
config = JSON.parse(File.read('config.json'))
puts config['database']['host']

# Read YAML
settings = YAML.load_file('settings.yaml')
puts settings['debug']

# Write JSON
File.write('output.json', JSON.pretty_generate(data))

Best practices for automation scripts

  • Use require_relative for loading local libraries
  • Add error handling with begin/rescue blocks
  • Use Ruby’s logging library for script output
  • Make scripts idempotent — running twice should be safe
  • Add shebang lines for direct execution
  • Handle signals (SIGINT, SIGTERM) for graceful shutdown
# Graceful shutdown handling
trap('SIGINT') do
  puts "\nReceived interrupt. Finishing current task..."
  exit 0
end

When to use Ruby for automation

Ruby excels at:

  • File and directory processing
  • Text manipulation and parsing
  • Building CLI tools
  • Quick prototyping of automation ideas
  • System administration tasks

For heavy-duty data processing, consider combining Ruby with command-line tools like awk, sed, and sort via Ruby’s system integration.

Summary

Ruby provides a powerful yet readable way to automate tasks. Its file handling, system command integration, and rich standard library make it ideal for DevOps scripting. Start with simple scripts and gradually add complexity as needed.

How to structure a useful script

A good automation script usually has one job, one obvious entry point, and one clear way to tell whether it worked. That is why the examples in this guide keep the top of the file simple: read configuration, load any required libraries, do the work, and print a short status message. If a script starts with a long chain of setup logic, it becomes harder to reuse and harder to test.

The best scripts also make their assumptions visible. If a script expects a filename, a directory, or a command-line flag, say so early and fail fast when the input is missing. That saves time during day-to-day use because callers do not have to guess what the script needs. It also keeps the error message close to the problem instead of hiding it several steps later.

Another good habit is to separate data gathering from data transformation. Read files or environment variables first, then transform that data into the final output, and only then write or print the result. That makes the script easier to debug because you can inspect each stage on its own. It also gives you a natural place to add logging if the script ever needs to run unattended.

When the work starts to feel like a small application rather than a script, that is usually the right time to split the logic into methods or plain Ruby classes. The command-line wrapper can stay thin, while the reusable code lives somewhere you can test without shelling out or waiting for cron. That split keeps the script friendly for quick tasks and still leaves room for growth.

When Ruby is a better fit than shell

Shell scripts are excellent for glue code, but Ruby becomes a better fit once the workflow needs richer data handling. If you are parsing JSON, rewriting configuration files, grouping records, or combining several command outputs, Ruby usually reads more clearly than nested shell pipelines. The language gives you arrays, hashes, exceptions, and standard libraries without making you reach for external tools first.

Ruby is also easier to maintain when a script has to be shared across a team. A clear method name, a small helper class, or a well-named variable usually communicates intent better than a chain of pipes and subshells. That matters when someone else needs to revisit the script months later and decide whether to patch it, extend it, or turn it into a task runner.

Even so, Ruby should not replace every shell command. Simple filesystem operations, one-line text filters, and small cron jobs can stay in Bash or POSIX shell if they already work well. The practical rule is to move to Ruby when the script needs structure, error handling, or data manipulation that is awkward in shell. That is where Ruby pays off quickly.

If you want to keep building on the same automation flow, move next to Automating Tasks with Rake for project-level task runners and Building CLI Tools with Thor for interactive command-line tools. Those two topics fit naturally after the scripting basics here because they reuse the same habits, but package them in more structured interfaces.

That is the point where Ruby stops feeling like a simple script runner and starts feeling like a small automation platform. You keep the same syntax, but the shape of the code becomes more deliberate and easier to reuse.

Environment variables and configuration

Automation scripts often need to read environment variables and handle configuration:

require 'json'

# Read environment variables
api_key = ENV['API_KEY']
database_url = ENV['DATABASE_URL'] || 'localhost:5432'

# Set default values
debug_mode = ENV.fetch('DEBUG', 'false') == 'true'

# Load configuration from JSON file
config = JSON.parse(File.read('config.json')) if File.exist?('config.json')

# Use environment variables in your script
puts "Running in #{ENV.fetch('RAILS_ENV', 'development')} mode" if debug_mode

Scheduling and cron integration

Ruby scripts work great with cron for scheduled automation. The backup example below reads configuration from environment variables, uses FileUtils.mkdir_p to create the backup directory if it does not exist yet, shells out to tar for the actual compression, and cleans up old backups by keeping only the seven most recent files:

#!/usr/bin/env ruby
# backup.rb - Example backup script for cron

require 'fileutils'
require 'time'

# Configuration
BACKUP_DIR = ENV['BACKUP_DIR'] || '/tmp/backups'
SOURCE_DIR = ARGV[0] || '/var/www'

timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_name = "backup_#{timestamp}.tar.gz"
backup_path = File.join(BACKUP_DIR, backup_name)

# Create backup directory if needed
FileUtils.mkdir_p(BACKUP_DIR)

# Create backup (using system tar command)
system('tar', '-czf', backup_path, '-C', SOURCE_DIR, '.')

if $?.success?
  puts "Backup created: #{backup_path}"
  # Clean old backups (keep last 7)
  Dir.glob(File.join(BACKUP_DIR, 'backup_*.tar.gz'))
     .sort[0...-7]
     .each { |f| File.delete(f) }
else
  puts "Backup failed!"
  exit 1
end

To run this daily at 2 AM, add to your crontab. The cron line specifies the full path to the Ruby interpreter and the script file, along with any arguments the script needs. Using absolute paths inside cron entries is a good habit because cron runs with a minimal environment:

0 2 * * * /usr/bin/ruby /path/to/backup.rb /data/to/backup

That last example is where Ruby scripts shine in real life. The file stays readable, the backup path is explicit, and the script can still handle logging or cleanup in pure Ruby instead of bolting together several shell commands.

Error handling in scripts

Reliable scripts handle errors gracefully:

begin
  # Try to process the file
  content = File.read(input_file)
  processed = process_content(content)
  File.write(output_file, processed)
rescue Errno::ENOENT => e
  puts "Error: File not found - #{e.message}"
  exit 1
rescue Errno::EACCES => e
  puts "Error: Permission denied - #{e.message}"
  exit 2
rescue StandardError => e
  puts "Error: #{e.message}"
  puts e.backtrace if ENV['DEBUG']
  exit 3
ensure
  puts "Script finished at #{Time.now}"
end

This pattern ensures your script provides useful error messages and exits with appropriate codes.