Table of Contents
- Why Ruby for Shell Scripting?
- Getting Started: Your First Ruby Shell Script
- Core Concepts for System Automation
- Practical Examples: Automate Real-World Tasks
- Advanced Techniques
- Best Practices for Ruby Shell Scripting
- Essential Tools & Libraries
- Conclusion
- References
Why Ruby for Shell Scripting?
Before diving in, let’s address the elephant in the room: Why use Ruby instead of Bash, Python, or Perl? Here’s why Ruby shines for system automation:
- Readable Syntax: Ruby’s English-like syntax makes scripts easy to write and maintain. For example,
FileUtils.cp('source.txt', 'dest/')is more intuitive thancp source.txt dest/in Bash (especially for complex operations). - Rich Standard Library: Ruby’s
stdlibincludes modules likeFileUtils(file operations),Open3(advanced command execution), andOptionParser(CLI argument parsing) out of the box—no need for external dependencies. - Object-Oriented Power: Model complex tasks with classes/objects (e.g., a
BackupManagerclass to handle backups). - Cross-Platform Compatibility: Write once, run on Linux, macOS, and even Windows (with Ruby installed).
- Exception Handling: Gracefully handle errors (e.g., missing files, permission issues) with
begin/rescueblocks, avoiding script crashes.
Getting Started: Your First Ruby Shell Script
Let’s write a simple script to verify Ruby is set up and demonstrate basic execution.
Step 1: Check Ruby Installation
Ensure Ruby is installed:
ruby -v # Should output something like "ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]"
If not, install Ruby via rbenv, rvm, or your system’s package manager (e.g., sudo apt install ruby on Ubuntu).
Step 2: Write Your First Script
Create a file named hello_automation.rb with:
#!/usr/bin/env ruby
# hello_automation.rb
# Get the current user (uses environment variable)
user = ENV['USER'] || 'there'
# Print a greeting
puts "Hello, #{user}! 👋 Welcome to Ruby shell scripting."
# List files in the current directory (using backticks to run system commands)
puts "\nHere are your files:"
puts `ls -la`
Step 3: Run the Script
Make the script executable and run it:
chmod +x hello_automation.rb
./hello_automation.rb
Output:
Hello, alice! 👋 Welcome to Ruby shell scripting.
Here are your files:
total 16
drwxr-xr-x 2 alice alice 4096 Sep 10 14:30 .
drwxr-xr-x 5 alice alice 4096 Sep 10 14:25 ..
-rwxr-xr-x 1 alice alice 267 Sep 10 14:30 hello_automation.rb
Key Takeaways:
- The shebang line
#!/usr/bin/env rubytells the system to run the script with Ruby. ENV['USER']accesses environment variables (fallback to ‘there’ if undefined).- Backticks (
`ls -la`) execute system commands and return their output as a string.
Core Concepts for System Automation
To build robust scripts, master these fundamentals:
1. Executing System Commands
Ruby offers multiple ways to run shell commands; choose based on your needs:
| Method | Use Case | Returns | Example |
|---|---|---|---|
Backticks (`) | Capture output (stdout) | String (output) | files = ls“ |
system('cmd') | Check success/failure (exit status) | true (success), false/nil | success = system('mkdir backups') |
%x{cmd} | Alias for backticks (more readable) | String (output) | disk_usage = %x{df -h} |
Open3 (stdlib) | Full control (stdin/stdout/stderr/exit) | Multiple values | See example below |
Advanced: Open3 for Full Control
Use Open3 to capture stdout, stderr, and exit status separately (avoids “hidden” errors):
require 'open3'
stdout, stderr, status = Open3.capture3('ls -la non_existent_dir')
puts "Output: #{stdout}"
puts "Errors: #{stderr}" # "ls: cannot access 'non_existent_dir': No such file or directory"
puts "Exit status: #{status.exitstatus}" # 2 (non-zero = failure)
2. File & Directory Operations
Avoid raw system('cp ...') commands—use Ruby’s built-in FileUtils (part of stdlib) for safer, cross-platform file handling:
require 'fileutils'
# Create a directory (with parent dirs if needed)
FileUtils.mkdir_p('backups/2024-09-10') # `-p` = "parent" (no error if exists)
# Copy files
FileUtils.cp('data.csv', 'backups/2024-09-10/')
# Move/rename files
FileUtils.mv('old_logs.txt', 'archive/')
# Delete files/dirs (use with caution!)
FileUtils.rm('temp.txt') # Delete file
FileUtils.rm_rf('tmp/') # Delete dir recursively (`-rf` = force)
# Symlink
FileUtils.ln_s('source.txt', 'link_to_source')
# Check if a file exists
if File.exist?('critical.config')
puts "Config file found!"
end
3. Input/Output Handling
Read from files, write to logs, or process user input:
Reading Files
# Read entire file into a string
content = File.read('notes.txt')
# Read line-by-line (memory-efficient for large files)
File.foreach('large_log.log') do |line|
puts line if line.include?('ERROR') # Print error lines
end
Writing Files
# Write to a file (overwrites existing content)
File.write('greeting.txt', "Hello from Ruby!\n")
# Append to a file
File.open('log.txt', 'a') do |file| # 'a' = append mode
file.puts "[#{Time.now}] Script ran successfully"
end
User Input
Read from STDIN (standard input) for interactive scripts:
print "Enter your name: "
name = gets.chomp # `gets` reads input; `chomp` removes newline
puts "Hello, #{name}!"
3. Environment Variables & Configuration
Store sensitive data (API keys) or script settings in environment variables or config files:
# Access environment variables
api_key = ENV['MY_APP_API_KEY'] || 'default_key'
# Load config from a YAML file (install `yaml` gem if needed)
require 'yaml'
config = YAML.load_file('config.yaml') # config.yaml: { "backup_dir": "backups" }
backup_dir = config['backup_dir']
4. Error Handling & Debugging
Prevent scripts from crashing with begin/rescue blocks:
require 'fileutils'
begin
FileUtils.cp('important.txt', 'backups/')
puts "Backup successful!"
rescue Errno::ENOENT => e # File not found
puts "Error: #{e.message} (file missing)"
rescue Errno::EACCES => e # Permission denied
puts "Error: #{e.message} (check permissions)"
rescue => e # Catch-all for other errors
puts "Unexpected error: #{e.message}"
end
Debugging Tip: Use puts or pp (pretty-print) to inspect variables:
require 'pp'
data = { name: 'Ruby', features: ['OOP', 'Readable'] }
pp data # Pretty-printed hash (easier to read than `puts data`)
Practical Examples: Automate Real-World Tasks
Let’s build scripts for common system tasks.
Example 1: Automated File Backup
Goal: Zip a directory and save it to a timestamped backup folder.
#!/usr/bin/env ruby
require 'fileutils'
require 'time'
# Configuration
SOURCE_DIR = '/home/alice/documents' # Dir to back up
BACKUP_ROOT = '/home/alice/backups'
TIMESTAMP = Time.now.strftime('%Y%m%d_%H%M%S') # e.g., 20240910_153045
BACKUP_DIR = "#{BACKUP_ROOT}/doc_backup_#{TIMESTAMP}"
begin
# Create backup dir
FileUtils.mkdir_p(BACKUP_DIR)
# Zip the source directory
zip_file = "#{BACKUP_DIR}/docs.zip"
puts "Zipping #{SOURCE_DIR} to #{zip_file}..."
system("zip -r #{zip_file} #{SOURCE_DIR}")
# Verify backup
if File.exist?(zip_file) && File.size(zip_file) > 0
puts "✅ Backup created: #{zip_file}"
else
raise "Backup failed: Zip file missing or empty"
end
rescue => e
puts "❌ Backup failed: #{e.message}"
exit 1 # Exit with non-zero status (signals failure to cron/systemd)
end
Usage: Run manually or schedule with cron (daily backups at 2 AM):
# Add to crontab (run `crontab -e`)
0 2 * * * /home/alice/scripts/backup_docs.rb >> /var/log/backup.log 2>&1
Example 2: Log File Analyzer
Goal: Count ERROR/WARN entries in a log file and generate a report.
#!/usr/bin/env ruby
# Usage: ./analyze_log.rb app.log
log_file = ARGV[0] || 'app.log' # ARGV[0] = first command-line argument
unless File.exist?(log_file)
puts "Error: Log file '#{log_file}' not found."
exit 1
end
errors = 0
warnings = 0
File.foreach(log_file) do |line|
errors += 1 if line.include?('[ERROR]')
warnings += 1 if line.include?('[WARN]')
end
report = <<~REPORT
Log Analysis Report (#{log_file})
==============================
Errors: #{errors}
Warnings: #{warnings}
Total issues: #{errors + warnings}
REPORT
puts report
File.write('log_report.txt', report) # Save to file
Usage: ./analyze_log.rb /var/log/nginx/error.log
Example 3: System Cleanup Utility
Goal: Delete files older than 30 days in a directory (e.g., downloads).
#!/usr/bin/env ruby
require 'fileutils'
# Cleanup dir and age threshold (days)
CLEANUP_DIR = '/home/alice/Downloads'
DAYS_OLD = 30
cutoff_time = Time.now - (DAYS_OLD * 24 * 60 * 60) # 30 days in seconds
puts "Cleaning files older than #{DAYS_OLD} days in #{CLEANUP_DIR}..."
Dir.glob("#{CLEANUP_DIR}/*") do |path|
next unless File.file?(path) # Skip directories
file_age = File.mtime(path) # Last modified time
if file_age < cutoff_time
puts "Deleting: #{path}"
FileUtils.rm(path)
end
end
puts "Cleanup complete!"
Example 4: System Health Reporter
Goal: Check CPU usage, disk space, and memory, then alert if thresholds are exceeded.
#!/usr/bin/env ruby
require 'open3'
# Thresholds (adjust as needed)
CPU_THRESHOLD = 80 # %
DISK_THRESHOLD = 85 # %
MEM_THRESHOLD = 90 # %
def check_cpu
# `top` output varies by OS; use `mpstat` (Linux) or `sysctl` (macOS)
stdout, = Open3.capture3('mpstat 1 1') # 1 sample, 1 second delay
idle = stdout.scan(/(\d+\.\d+)\s*$/).last.first.to_f # Extract idle %
usage = 100 - idle
{ usage: usage.round(1), ok: usage < CPU_THRESHOLD }
end
def check_disk
stdout, = Open3.capture3('df -h /') # Check root filesystem
used_pct = stdout.split("\n")[1].split[4].to_i # Extract used %
{ usage: used_pct, ok: used_pct < DISK_THRESHOLD }
end
# Run checks
cpu = check_cpu
disk = check_disk
# Generate report
report = "System Health Check\n"
report += "CPU Usage: #{cpu[:usage]}% (#{cpu[:ok] ? 'OK' : 'ALERT!'})\n"
report += "Disk Usage: #{disk[:usage]}% (#{disk[:ok] ? 'OK' : 'ALERT!'})\n"
puts report
# Send alert (e.g., email or Slack; use `mail` command or API)
unless cpu[:ok] && disk[:ok]
system("echo '#{report}' | mail -s 'System Alert' [email protected]")
end
Advanced Techniques
Command-Line Arguments with OptionParser
Use OptionParser (stdlib) to add flags like --help, --verbose, or --dir:
require 'optparse'
options = { verbose: false, dir: '.' }
parser = OptionParser.new do |opts|
opts.banner = "Usage: ./script.rb [options]"
opts.on('-v', '--verbose', 'Enable verbose output') { options[:verbose] = true }
opts.on('-d', '--dir DIR', 'Target directory') { |d| options[:dir] = d }
opts.on_tail('-h', '--help', 'Show this message') { puts opts; exit }
end
begin
parser.parse!(ARGV)
rescue OptionParser::InvalidOption => e
puts e.message
puts parser
exit 1
end
puts "Verbose: #{options[:verbose]}, Directory: #{options[:dir]}"
Usage: ./script.rb -v --dir /tmp
Parallel Processing
Speed up tasks (e.g., processing multiple files) with threads:
require 'thread'
files = Dir.glob('data/*.txt')
threads = []
files.each do |file|
threads << Thread.new(file) do |f| # Pass file to thread
puts "Processing #{f} (thread #{Thread.current.object_id})"
# Add heavy processing here (e.g., parsing large files)
end
end
threads.each(&:join) # Wait for all threads to finish
puts "All files processed!"
Best Practices for Ruby Shell Scripting
- Keep It Readable: Use meaningful variable names (
backup_dirvsbd). - Modularize: Split logic into methods/classes (e.g.,
def backup_file(path)). - Test: Use
minitestorrspecto test edge cases (e.g., missing files). - Secure Inputs: Sanitize user/command-line inputs to avoid shell injection:
# UNSAFE: User input directly in command # system("rm -rf #{user_input}") # Risky if user_input is '../' # SAFE: Use array syntax with Open3 (no shell interpolation) Open3.capture3('rm', '-rf', user_input) # `user_input` is treated as a literal - Avoid Overhead: Prefer Ruby methods (
FileUtils.cp) oversystem('cp')(faster, cross-platform).
Essential Tools & Libraries
Enhance scripts with these gems:
- Thor: Build CLI apps with subcommands (e.g.,
script backup,script restore). - TTY::Prompt: Interactive menus/checkboxes for user input.
- Colorize: Add color to output (
puts "Error".red). - Dotenv: Load environment variables from
.envfiles (no moreexportin shell).
Conclusion
Ruby shell scripting combines the power of a programming language with the simplicity of shell scripts. Its readable syntax, rich standard library, and cross-platform support make it ideal for automating system tasks—from backups to log analysis. By mastering core concepts like FileUtils, Open3, and error handling, and following best practices, you’ll write scripts that are maintainable, secure, and efficient.
Start small (e.g., a daily cleanup script), then level up with advanced tools like Thor or parallel processing. Happy automating!
References
- Ruby Official Documentation (FileUtils, Open3, OptionParser)
- Ruby Shell Scripting Guide
- Thor Gem
- FileUtils Cheatsheet
- Cron Job Scheduling