command_kit
Description
A modular Ruby toolkit for building clean, correct, and robust CLI commands as plain-old Ruby classes.
Features
- Simple - Commands are plain-old ruby classes, with options and arguments declared as attributes. All features are Ruby modules that can be included into command classes.
 - 
Correct - CommandKit behaves like a standard UNIX command.
- Safely handles Ctrl^C / SIGINT interrupts and exits with 130.
 - Safely handles broken pipes (aka 
mycmd | head). - Respects common environment variables (ex: 
TERM=dumbandNO_COLOR). - Uses OptionParser for POSIX option parsing.
 - Disables ANSI color when output is redirected to a file or when 
NO_COLORis set. 
 - 
Complete - Provides many additional CLI features.
- OS detection.
 - Terminal size detection.
 - ANSI coloring support.
 - Interactive input.
 - Rich text printing support (fields, lists, and tables).
 - Subcommands (explicit or lazy-loaded) and command aliases.
 - Displaying man pages for 
--help/help. - Using the pager (aka 
less). - 
XDG directories (aka 
~/.config/,~/.local/share/,~/.cache/). - Exception handling / Bug reporting.
 
 - 
Testable - Since commands are plain-old Ruby classes, it's easy to
initialize them and call 
#mainor#run. 
Anti-Features
- No additional runtime dependencies.
 - Does not implement it's own option parser.
 - Not named after a comic-book Superhero.
 
Requirements
- ruby >= 3.0.0
 
Install
$ gem install command_kitgemspec
gem.add_dependency 'command_kit', '~> 0.3'Gemfile
gem 'command_kit', '~> 0.3'Examples
lib/foo/cli/my_cmd.rb
require 'command_kit'
module Foo
  module CLI
    class MyCmd < CommandKit::Command
      usage '[OPTIONS] [-o OUTPUT] FILE'
      option :count, short: '-c',
                     value: {
                       type: Integer,
                       default: 1
                     },
                     desc: "Number of times"
      option :output, short: '-o',
                      value: {
                        type: String,
                        usage: 'FILE'
                      },
                      desc: "Optional output file"
      option :verbose, short: '-v', desc: "Increase verbose level" do
        @verbose += 1
      end
      argument :file, required: true,
                      usage: 'FILE',
                      desc: "Input file"
      examples [
        '-o path/to/output.txt path/to/input.txt',
        '-v -c 2 -o path/to/output.txt path/to/input.txt',
      ]
      description 'Example command'
      def initialize(**kwargs)
        super(**kwargs)
        @verbose = 0
      end
      def run(file)
        puts "count=#{options[:count].inspect}"
        puts "output=#{options[:output].inspect}"
        puts "file=#{file.inspect}"
        puts "verbose=#{@verbose.inspect}"
      end
    end
  end
endbin/my_cmd
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.expand_path('../../lib',__FILE__))
require 'foo/cli/my_cmd'
Foo::CLI::MyCmd.start--help
Usage: my_cmd [OPTIONS] [-o OUTPUT] FILE
Options:
    -c, --count INT                  Number of times (Default: 1)
    -o, --output FILE                Optional output file
    -v, --verbose                    Increase verbose level
    -h, --help                       Print help information
Arguments:
    FILE                             Input file
Examples:
    my_cmd -o path/to/output.txt path/to/input.txt
    my_cmd -v -c 2 -o path/to/output.txt path/to/input.txt
Example command
Testing
RSpec
require 'spec_helper'
require 'stringio'
require 'foo/cli/my_cmd'
describe Foo::CLI::MyCmd do
  let(:stdin)  { StringIO.new }
  let(:stdout) { StringIO.new }
  let(:stderr) { StringIO.new }
  let(:env)    { ENV }
  subject do
    described_class.new(
      stdin:   stdin,
      stdout:  stdout,
      stderr:  stderr,
      env:     env
    )
  end
  # testing with raw options/arguments
  describe "#main" do
    context "when executed with no arguments" do
      it "must exit with -1" do
        expect(subject.main([])).to eq(-1)
      end
    end
    context "when executed with -o OUTPUT" do
      let(:file)   { ... }
      let(:output) { ... }
      before { subject.main(["-o", output, file]) }
      it "must create the output file" do
        ...
      end
    end
  end
endReference
- CommandKit::Arguments
 - CommandKit::BugReport
 - CommandKit::Colors
 - CommandKit::Command
 - CommandKit::CommandName
 - CommandKit::Commands
 - CommandKit::Completion::Install
 - CommandKit::Description
 - CommandKit::Edit
 - CommandKit::Env
 - CommandKit::Examples
 - CommandKit::ExceptionHandler
 - CommandKit::FileUtils
 - CommandKit::Help
 - CommandKit::Interactive
 - CommandKit::Main
 - CommandKit::Open
 - CommandKit::Options
 - CommandKit::Pager
 - CommandKit::Printing
 - CommandKit::ProgramName
 - CommandKit::Stdio
 - CommandKit::Terminal
 - CommandKit::Usage
 - CommandKit::XDG
 
Real-World Examples
Alternatives
Special Thanks
Special thanks to everyone who answered my questions and gave feedback on Twitter.
Copyright
Copyright (c) 2021-2025 Hal Brodigan
See {file:LICENSE.txt} for details.