Lesson 23 / 25

Automation, Rake and Command-Line Tools

Write maintainable scripts, Rake tasks and command-line tools in Ruby.

Ruby as a glue language

Ruby is a pleasant language for automation: renaming files, generating reports, calling APIs, migrating data or wrapping shell commands. Keep scripts maintainable: put logic in methods or small classes, read configuration from ENV and arguments, exit with meaningful status codes (exit 1 or abort "message"), and print errors to $stderr. Parse options with the standard OptionParser or gems such as Thor. Run shell commands safely with Open3.capture3("cmd", arg1, arg2), passing arguments separately so they are not interpreted by a shell, rather than backticks with interpolated user input. Rake (Ruby make) defines tasks with dependencies in a Rakefile: task :report => :fetch do ... end, run with rake report; Rails uses Rake for many maintenance tasks. For distributable tools, package them as gems with an executable in exe/. Scripts that will run in CI or cron should be idempotent and log what they do.

A small CLI with OptionParser and Open3

Options, safe subprocess calls and proper exit codes.

#!/usr/bin/env ruby
# frozen_string_literal: true

require "optparse"
require "open3"

options = { days: 7, dry_run: false }
OptionParser.new do |opts|
  opts.banner = "Usage: cleanup_logs [options] DIR"
  opts.on("-d", "--days N", Integer, "Delete logs older than N days") { options[:days] = _1 }
  opts.on("-n", "--dry-run", "Only print what would be deleted") { options[:dry_run] = true }
end.parse!

dir = ARGV.first or abort("error: DIR is required")
abort("error: #{dir} is not a directory") unless File.directory?(dir)

cutoff = Time.now - (options[:days] * 86_400)
old_logs = Dir.glob(File.join(dir, "*.log")).select { File.mtime(_1) < cutoff }

old_logs.each do |path|
  if options[:dry_run]
    puts "would delete #{path}"
  else
    File.delete(path)
  end
end

stdout, stderr, status = Open3.capture3("df", "-h", dir)   # arguments passed separately, no shell
$stderr.puts(stderr) unless status.success?
puts stdout
exit(status.success? ? 0 : 1)

# Rakefile:
# task :cleanup do
#   sh "ruby", "bin/cleanup_logs", "--days", "14", "log"
# end

A versatile kitchen helper

Ruby scripts are the kitchen helper who can chop, measure, label and tidy up between the big appliances. Rake is the list on the fridge saying which jobs depend on which.

Quick check: Why prefer Open3.capture3("cmd", arg) over a backtick command with interpolated input?

  • It is shorter
  • Arguments are passed directly without a shell, preventing command injection, and you get stdout, stderr and status
  • Backticks do not work in Ruby
  • It runs faster on every system
Answer

Arguments are passed directly without a shell, preventing command injection, and you get stdout, stderr and status — Passing arguments separately avoids shell interpretation of untrusted input.