7 Ruby Gems That Transform Your Command-Line Scripts Into Professional CLI Tools

Discover 7 essential Ruby gems for building polished CLI tools. From OptionParser to TTY::Prompt, learn which gem fits your project with real code examples.

7 Ruby Gems That Transform Your Command-Line Scripts Into Professional CLI Tools

I’ve been writing Ruby scripts for years, and I still remember my first command-line tool—a messy collection of ARGV checks and puts statements that only I could understand. Over time, I learned that building a proper CLI doesn’t mean reinventing the wheel. Ruby has a rich ecosystem of gems that turn a few lines of code into a polished, user-friendly interface. In this article, I’ll walk you through seven gems I’ve used to build everything from simple file utilities to complex deployment tools. I’ll keep things simple, because when you’re starting, you don’t need jargon—you need working code.

Let me show you what I’ve learned, one gem at a time.

The built-in starter: OptionParser

Before I ever installed a third-party gem, I used OptionParser. It’s part of Ruby’s standard library, so you already have it. The thing is, most beginners ignore it because they think parsing arguments is hard. It’s not. OptionParser gives you a clean way to define flags, switches, and help text without writing endless if statements.

Here’s how I started a simple file size calculator:

require 'optparse'
require 'fileutils'

options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: sizer.rb [options]"

  opts.on("-f", "--file FILE", "Path to file") do |f|
    options[:file] = f
  end

  opts.on("-u", "--unit UNIT", [:b, :kb, :mb], "Unit: b, kb, mb") do |u|
    options[:unit] = u
  end

  opts.on("-h", "--help", "Prints help") do
    puts opts
    exit
  end
end.parse!

if options[:file]
  size = File.size(options[:file])
  case options[:unit]
  when :kb then size /= 1024.0
  when :mb then size /= 1024.0 / 1024.0
  end
  puts "Size: #{size} #{options[:unit] || 'bytes'}"
else
  puts "No file specified. Use -h for help."
end

That script gave me a structure I could reuse. I added an --all flag to loop through directories. OptionParser handles --no- prefixes automatically—try --no-color if you define a boolean flag. It’s simple but powerful for short scripts. The downside? When your tool grows beyond ten flags, you start wanting something that organizes tasks better. That’s when I moved to Thor.

Thor: tasks and templates

Thor is the workhorse of Ruby CLI gems. Rails uses it for generators. It’s built around tasks—think of them as subcommands. If you’ve ever typed thor help generate, you know what I mean. Thor also brings templates, file manipulation, and nice error handling.

My first Thor app was a project scaffold. I wanted to create a folder with a Gemfile, README.md, and a lib directory. Here’s the core:

require 'thor'
require 'fileutils'

class Scaffold < Thor
  desc "new PROJECT_NAME", "Create a new Ruby project"
  def new(name)
    dir = "./#{name}"
    FileUtils.mkdir_p("#{dir}/lib")
    FileUtils.mkdir_p("#{dir}/spec")
    write_gemfile(dir, name)
    write_readme(dir, name)
    puts "Project #{name} created!"
  end

  desc "add COMPONENT", "Add a component to existing project"
  def add(component)
    FileUtils.mkdir_p("./lib/#{component}")
    puts "Added #{component} component."
  end

  private

  def write_gemfile(dir, name)
    File.write("#{dir}/Gemfile", <<~RUBY)
      source 'https://rubygems.org'
      gem '#{name}'
    RUBY
  end

  def write_readme(dir, name)
    File.write("#{dir}/README.md", "# #{name}\n\nMy amazing project.")
  end
end

Scaffold.start(ARGV)

Run ruby scaffold.rb new my_app and it generates the structure. Thor picks up --help automatically. What I love most is how you can define group options and shared options—like a --dry-run flag for every task. For example:

class_option :verbose, type: :boolean, default: false, desc: "Enable verbose output"


def new(name)
  puts "Verbose mode on" if options[:verbose]
  # ... rest of code
end

Thor also has a map command to alias tasks. You can say map '-v' => :version to make -v show version. It’s not the simplest gem to learn, but once you get past the desc and method_option syntax, you’ll never go back to raw ARGV.

Commander: help text made pretty

I discovered Commander when I needed a CLI that looked professional out of the box. It gives you a global program object where you define version, description, and commands. The generated help output is beautiful—colored, aligned, and easy to read.

Here’s a git-like tool I built with Commander:

require 'commander'
require 'json'

module Gitlike
  class Application
    include Commander::Methods

    def run
      program :name, 'gitlike'
      program :version, '0.1.0'
      program :description, 'A toy version control tool'

      command :init do |c|
        c.syntax = 'gitlike init'
        c.description = 'Initialize a new repository'
        c.action do |args, options|
          Dir.mkdir('.gitlike') unless Dir.exist?('.gitlike')
          File.write('.gitlike/config.json', JSON.pretty_generate({created: Time.now}))
          puts "Initialized empty Gitlike repository"
        end
      end

      command :status do |c|
        c.syntax = 'gitlike status'
        c.description = 'Show working tree status'
        c.action do |args, options|
          if Dir.exist?('.gitlike')
            puts "Repository exists. (Real status would check file hashes here.)"
          else
            puts "Not a gitlike repository"
          end
        end
      end

      default_command :help
      run!
    end
  end
end

Gitlike::Application.new.run

Commander uses run! to parse ARGV and dispatch. The method command blocks let me define options per command. I added a global option --json to output results as JSON by checking options.json inside each action. The gem also has a global_option method. One thing I often do is set up a --verbose flag and then redirect $stdout to a log file if needed. Commander handles the rest.

The only catch is that Commander is less maintained than Thor. But for small tools that need quick polish, it’s fantastic.

GLI: for apps with nested subcommands

GLI stands for “Git-Like Interface.” That exactly describes what it builds. If you have commands that need deeper nesting—like myapp config set key value—GLI gives you a structure for that. It also generates a skeleton when you run gli init.

I used GLI to build a note-taking tool with categories:

require 'gli'
include GLI::App

program_desc 'A simple note taker'

flag [:category, :c], default_value: 'default'

command :add do |c|
  c.desc 'Add a new note'
  c.arg_name 'TITLE'
  c.action do |global_options, options, args|
    title = args.join(' ')
    cat = global_options[:category]
    File.open("#{cat}_notes.txt", 'a') { |f| f.puts "#{Time.now}: #{title}" }
    puts "Note added to #{cat}."
  end
end

command :list do |c|
  c.desc 'List all notes'
  c.action do |global_options, options, args|
    cat = global_options[:category]
    if File.exist?("#{cat}_notes.txt")
      puts File.read("#{cat}_notes.txt")
    else
      puts "No notes in #{cat}."
    end
  end
end

exit run(ARGV)

GLI’s flag and switch methods are straightforward. The run method returns an exit status. I like that you can define a pre block that runs before every command—handy for checking config files or auth tokens. For example:

pre do |global, command, options, args|
  unless File.exist?('config.yml')
    puts "Config missing. Run 'setup' first."
    exit 1
  end
end

GLI has a steeper learning curve because of its DSL. But once you internalize that command blocks are just method definitions, it clicks.

Slop: the minimalist’s friend

Sometimes you don’t need a full framework. You just need to parse flags without ceremony. That’s Slop. It’s a single file, no dependencies, and you can wrap your entire script in a few lines.

I wrote a quick file renamer with Slop:

require 'slop'

opts = Slop.parse do |o|
  o.banner = "Usage: rename.rb [options]"
  o.string '-p', '--pattern', 'Search pattern (regex)'
  o.string '-r', '--replacement', 'Replacement string'
  o.boolean '-d', '--dry-run', 'Show what would change'
  o.on '--help' do
    puts o
    exit
  end
end

Dir.glob('*').each do |file|
  new_name = file.gsub(Regexp.new(opts[:pattern]), opts[:replacement])
  next if new_name == file
  if opts.dry_run?
    puts "Would rename #{file} to #{new_name}"
  else
    File.rename(file, new_name)
    puts "Renamed #{file} to #{new_name}"
  end
end

Slop’s parse returns an object where you access values with opts[:pattern] or opts.pattern. It supports automatic type casting—opts.integer '-n' returns a number. I love how Slop doesn’t force a structure. You can embed it inside a loop or a class.

The downside? Slop doesn’t generate help text as neatly as Commander. And it has no built-in support for subcommands. But for one-off scripts that you want to share with coworkers, it’s perfect.

HighLine: asking questions with style

Command-line interactions shouldn’t be ugly. HighLine makes it easy to ask for input, display lists, and colorize output. It’s not a parser—it’s a user interaction gem. I pair it with any of the above parsers.

Here’s a simple contact saver using HighLine:

require 'highline/import'
require 'json'

contacts = []
loop do
  name = ask("Name? ") { |q| q.validate = /\A\w+\z/ }
  phone = ask("Phone? ") { |q| q.validate = /\A\d{10}\z/ }
  contacts << { name: name, phone: phone }
  break unless agree("Add another? (y/n) ")
end

File.write('contacts.json', JSON.pretty_generate(contacts))
puts "Saved #{contacts.length} contacts."

ask can take a block where you set validation rules, default values, and even completions. There’s also choose for menus:

choice = choose("Select an action:", "View", "Edit", "Delete")

HighLine handles ANSI colors with say("<%= color('Success', GREEN) %>"). I use it to highlight errors in red. The gem also reads passwords without echoing.

One trick I learned: use HighLine.new to create an instance instead of importing everything into global namespace. That keeps your code clean:

cli = HighLine.new
name = cli.ask("Name?")

HighLine doesn’t parse command-line arguments—you still need OptionParser for that. But for interactive prompts during a command, it’s unmatched.

TTY::Prompt: modern interactive arrays

The TTY toolkit is a collection of gems. The one I use most is tty-prompt. It gives you beautiful, interactive prompts with keyboard navigation. Think of it as HighLine’s younger, more modern sibling.

I built a configuration wizard with TTY::Prompt:

require 'tty-prompt'
require 'yaml'

prompt = TTY::Prompt.new

project_type = prompt.select("Choose project type?", %w(gem rails script))
language = prompt.select("Primary language?", %w(ruby javascript python), default: 1)
name = prompt.ask("Project name?", required: true) { |q| q.validate(/\A[a-z_]+\z/, "Use snake_case") }
use_docker = prompt.yes?("Add Docker support?")

config = {
  type: project_type,
  lang: language,
  name: name,
  docker: use_docker
}
File.write('project_config.yml', config.to_yaml)
puts "Config saved."

The select menu lets you move with arrow keys. multi_select allows picking multiple options. ask has validators, masks for passwords, and autocomplete. The gem also includes slider, keypress, and collect for multi-step forms.

I combine TTY::Prompt with Thor. Inside a Thor task, I call TTY::Prompt.new.ask(...) to get interactive input. The visual feedback—like checking items—makes the tool feel professional.

One thing to note: TTY::Prompt depends on pastel and tty-cursor for colors and cursor movement. It works on Windows too, but you might need the win32console gem on older Ruby versions.

Choosing the right gem for the job

I don’t use the same gem for every project. My rule of thumb:

If my tool has only three flags and no subcommands, I use OptionParser. It’s built-in, zero dependencies, and my code remains readable.

If I need a set of tasks that share options and maybe generate files, I reach for Thor. It’s the Swiss Army knife.

If I want a beautiful help page without much code, Commander gets the job done in half the lines.

If my command structure is deeply nested, like a git clone, I use GLI. Its pre and post hooks let me centralize validation.

If I’m writing a throwaway script for a team, Slop keeps it small. I don’t want to burden them with a list of gems.

If I need to ask users for input during execution, HighLine gives me validation and colors with minimal setup.

If I want a modern, interactive wizard, TTY::Prompt is the way. It’s my go-to for installation scripts or setup tools.

You can mix them. For example, use Thor for the outer structure and TTY::Prompt for interactive prompts inside each task. I’ve done that many times. The gems coexist because they don’t step on each other’s toes—one handles parsing, the other handles I/O.

My personal workflow

I often start a new CLI with a single file that uses OptionParser. Then I realize I need subcommands, so I refactor to Thor. Once I add interactive input, I bring in TTY::Prompt. I keep the code organized into a lib folder with a main runner class.

Here’s a template I use:

# bin/myapp
#!/usr/bin/env ruby
require_relative '../lib/myapp/cli'
MyApp::CLI.new.run

Inside lib/myapp/cli.rb:

require 'thor'
require 'tty-prompt'
require_relative 'commands'

module MyApp
  class CLI < Thor
    desc "setup", "Run setup wizard"
    def setup
      Commands::Setup.new.run
    end

    desc "status", "Show current status"
    method_option :json, type: :boolean, desc: "Output as JSON"
    def status
      Commands::Status.new(options).run
    end

    desc "version", "Show version"
    def version
      puts "MyApp #{MyApp::VERSION}"
    end
  end
end

Then Commands::Setup uses TTY::Prompt, and Commands::Status uses the options hash. This separation keeps each file under 50 lines.

Beyond the seven

There are other gems I’ve tried: Slop by Lee Jarvis, Methadone (which wraps OptionParser with logging), Commander by TJ Holowaychuk (now maintained by others), and Ruby's standard library RDoc for documentation. I also use Pastel for colors directly when I don’t need a full prompt library.

But the seven I covered here—OptionParser, Thor, Commander, GLI, Slop, HighLine, and TTY::Prompt—cover 99% of CLI scenarios. You can build anything from a file converter to a cloud orchestrator.

Final thoughts

Building a command-line application in Ruby doesn’t have to be complicated. Start small, with one gem. Add another when you need it. Write code that other people can run without reading a manual. That’s the whole point of a CLI—it’s a tool that does one thing well.

I still go back to my first awful script from time to time. It had no help, no flags, and if you typed the wrong argument, it just crashed. Now I look at my projects and see clean YAML configurations, colorful prompts, and error messages that actually help. The gems made that possible. But more than that, understanding how they work—deep down, they all parse strings and call methods—gave me the confidence to write tools that my teammates actually use.

You can do it too. Pick one gem today. Write a script that prints “Hello, World” with an option for different languages. Then add a subcommand. Then make it interactive. Before you know it, you’ll have a CLI that feels like it was built by a professional.

I promise, it’s easier than it looks. And the feeling of typing myapp do-something --fast and seeing it work is worth every line of code.


// Keep Reading

Similar Articles

Mastering Rust Closures: Boost Your Code's Power and Flexibility
Ruby

Mastering Rust Closures: Boost Your Code's Power and Flexibility

Rust closures capture variables by reference, mutable reference, or value. The compiler chooses the least restrictive option by default. Closures can capture multiple variables with different modes. They're implemented as anonymous structs with lifetimes tied to captured values. Advanced uses include self-referential structs, concurrent programming, and trait implementation.

Read Article →