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=dumb
andNO_COLOR
). - Uses OptionParser for POSIX option parsing.
- Disables ANSI color when output is redirected to a file or when
NO_COLOR
is 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
#main
or#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_kit
gemspec
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=#{[:count].inspect}"
puts "output=#{[:output].inspect}"
puts "file=#{file.inspect}"
puts "verbose=#{@verbose.inspect}"
end
end
end
end
bin/my_cmd
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.('../../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
end
Reference
- 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
Alternatives
Special Thanks
Special thanks to everyone who answered my questions and gave feedback on Twitter.
Copyright
Copyright (c) 2021-2024 Hal Brodigan
See LICENSE for details.