A small Ruby library for parsing command-line flags (short and long options with values). Inspired by Go's flag package, this library provides a simple, composable approach to CLI option parsing without the complexity of subcommands.
Note: This library does not support subcommands. It focuses on simple flag parsing for single-command CLI tools.
This library was developed with a singular mission: to liberate the Presentation Layer (CLI interactions) from the rigid constraints of library internals. By choosing composition over inheritance, simple-cli-options ensures that your application remains in control of its own structure and logic.
Many Ruby CLI frameworks require you to inherit from a specific base class. While this might seem convenient at first, it creates a "Strict Binding" where your presentation logic becomes shackled to the library’s internal implementation. This leads to several critical issues that simple-cli-options intentionally avoids:
- No Inheritance Trap: Your CLI logic is no longer forced to conform to the "hidden rules" of a parent class. This prevents the library's internal evolution from accidentally breaking your application's behavior.
- Decoupled Presentation Layer: We treat CLI interaction as a distinct presentation layer. By avoiding inheritance, we ensure this layer remains independent and isn't "fused" with the library’s parsing engine.
- Dependency Minimization: In this model, the library is a tool, not a foundation. You use it when and where you need it, rather than building your entire application inside it. This significantly reduces your long-term dependency on the library’s specific versioning or quirks.
- True Implementation Freedom: Without the "gravity" of a parent class pulling your design in a certain direction, you are free to organize your CLI interactions in a way that best serves your users, not the framework.
simple-cli-options is built for developers who want a powerful parser without the "contractual baggage" of a framework. It provides the strictness of type-safe parsing (via Sorbet) and the flexibility of Ruby, all while keeping your presentation layer clean, independent, and future-proof.
See CHANGELOG.md for detailed version history and breaking changes.
Current version: 0.2.1
Note: This project is in beta. APIs may change in future releases.
After building the gem (gem build simple-cli-options.gemspec in the repo), install and require:
gem install simple-cli-options-0.2.1.gemrequire 'simple-cli-options'
# Option and Options are now availableUse the library as plain Ruby files. No gem packaging or Gemfile required.
- Clone or download the repo, or copy the
lib/folder (containingoption.rbandoptions.rb) into your project. - Install the runtime dependency (needed for type hints):
gem install sorbet-runtime
- Require the files from your script (adjust paths if needed):
If you keep the library in a subdirectory (e.g.
require_relative 'lib/option' require_relative 'lib/options'
vendor/simple-options/):require_relative 'vendor/simple-options/lib/option' require_relative 'vendor/simple-options/lib/options'
A stable gem may be published to RubyGems.org later.
If you use a Gemfile, add the dependency and require as above:
# Gemfile
gem 'sorbet-runtime'require 'simple-cli-options'
# Instantiate an Options object
parser = SimpleOptions::Options.new(
program_name: 'todo',
description: 'A simple todo list manager'
)
# Define options using type-specific methods
parser.boolean(:list, desc: 'Show list of todos')
parser.string(:add, desc: 'Add a new todo item')
parser.integer(:delete, desc: 'Delete todo by ID')
# Parse command-line arguments
parser.parse
# Get values
if parser.get(:list)
puts "Showing todo list..."
elsif parser.get(:add)
puts "Adding: #{parser.get(:add)}"
elsif parser.get(:delete)
puts "Deleting todo ##{parser.get(:delete)}"
endUsage examples:
# Show help (automatically supported)
ruby todo.rb -h
# Show list
ruby todo.rb -list
# Add a new task
ruby todo.rb -add "Buy groceries"
# Delete a task
ruby todo.rb -delete 3Specifying -h or --help automatically displays help and exits.
Represents a single option with short form (e.g., -l), long form (e.g., --length), description, type, validation, and conversion.
Option.new(name, desc:, short: '', long: '', required: false, default: nil, type: :string, **options)name(Symbol): Option namedesc(String): Description (required)short(String): Short flag form (optional)long(String): Long flag form (optional)required(Boolean): Whether the flag is required (default:false)default: Default value (optional)type(Symbol): Type (:integer,:number,:boolean,:string)**options: Additional options (:validate,:convert)
Important: If both short and long are omitted, -name is automatically used.
#name,#short,#long,#desc,#required_flag,#type: Read-only attributes
#validate(&block): Add a validator (block receives a string, returnsnilon success or an error message on failure). Returnsselffor chaining.#process(value): Run all validators and perform type conversion. RaisesArgumentErroron validation failure.
Example:
opt = SimpleOptions::Option.new(:port, desc: 'Port number', type: :integer)
.validate { |v| (1..65535).cover?(v.to_i) ? nil : 'Port must be between 1 and 65535' }
opt.process('8080') # => 8080 (Integer)
opt.process('99999') # => raises ArgumentErrorCollects options, parses command-line arguments, and provides access to parsed values.
Options.new(program_name: nil, description: nil)program_name(String): Program name (defaults to executable name if omitted)description(String): Program description
Concise methods for defining options:
# Integer type
parser.integer(:count, desc: 'Count', short: '', long: '', required: false, default: nil)
# Boolean type
parser.boolean(:verbose, desc: 'Verbose mode', short: '', long: '', required: false, default: nil)
# Number type (integer or float)
parser.number(:ratio, desc: 'Ratio', short: '', long: '', required: false, default: nil)
# String type
parser.string(:name, desc: 'Name', short: '', long: '', required: false, default: nil)
# Generic (type can be specified)
parser.option(:port, desc: 'Port', type: :integer, short: '', long: '', required: false, default: nil)Omitting default values:
- When
defaultis omitted, type-specific defaults are used:integer:0boolean:falsenumber:0string:''option:nil
#add(option): Register anOptionobject directly#parse(argv = nil): Parse arguments (defaults toARGV). If-h/--helpis present, displays help and exits#get(name): Get parsed value (Symbol). Returns default value if flag not specified#show_help: Display help
-h/--helpsupport: Automatically displays help and exits- Unspecified flag handling: Returns default value (including
nil) - Error handling: Displays error message and exits on missing required options or validation failures
When short and long are omitted, -name is automatically used:
parser = SimpleOptions::Options.new(
program_name: 'myapp',
description: 'My awesome application'
)
# Specify only name and desc
parser.string(:input, desc: 'Input file')
parser.boolean(:verbose, desc: 'Verbose output')
parser.parse
# Can be used like: -input file.txt -verboseparser.integer(:port, desc: 'Port number', default: 8080)
parser.parse(%w[-port 3000])
parser.get(:port) # => 3000 (Integer)The number type supports both integers and floating-point numbers:
parser.number(:ratio, desc: 'Ratio value')
parser.parse(%w[-ratio 3.14])
parser.get(:ratio) # => 3.14 (Float)
parser.parse(%w[-ratio 42])
parser.get(:ratio) # => 42 (Integer when no decimal point)Number type features:
- Integer format (
42) → Converted toInteger - Floating-point format (
3.14) → Converted toFloat - Automatically selects the appropriate type
parser.boolean(:debug, desc: 'Enable debug mode')
# Accepts the following values
parser.parse(%w[-debug true]) # => true
parser.parse(%w[-debug false]) # => false
parser.parse(%w[-debug yes]) # => true
parser.parse(%w[-debug no]) # => false
parser.parse(%w[-debug 1]) # => true
parser.parse(%w[-debug 0]) # => false
# Defaults to true when value is omitted
parser.parse(%w[-debug]) # => trueparser.string(:name, desc: 'User name', default: 'Guest')
parser.parse(%w[-name Alice])
parser.get(:name) # => "Alice" (String)Reflected in help display:
parser = SimpleOptions::Options.new(
program_name: 'mytool',
description: 'A tool for processing data'
)
parser.string(:file, desc: 'Input file')
parser.show_helpOutput:
mytool
A tool for processing data
Usage:
mytool [flags]
Flags:
-file Input file
parser = SimpleOptions::Options.new(description: 'Data processor')
# Required option
parser.string(:input, desc: 'Input file', required: true)
# Options with default values
parser.integer(:threads, desc: 'Number of threads', default: 4)
parser.string(:output, desc: 'Output file', default: 'output.txt')
parser.parse
# If required option is not specified, displays error message and exits
# Options with default values return the default when not specifiedparser = SimpleOptions::Options.new
parser.integer(:count, desc: 'Count', default: 10)
parser.string(:name, desc: 'Name') # No default value
parser.parse([]) # No arguments
parser.get(:count) # => 10 (default value)
parser.get(:name) # => "" (string type default)parser = SimpleOptions::Options.new
opt = parser.option(:size, desc: 'Size (s/m/l)', type: :string)
opt.validate { |v| %w[s m l].include?(v) ? nil : 'Size must be one of s/m/l' }
parser.parse(%w[-size m]) # OK
parser.parse(%w[-size xl]) # Error: Size must be one of s/m/lparser = SimpleOptions::Options.new
# Specify both short and long
parser.integer(:verbose, desc: 'Verbosity level', short: '-v', long: '--verbose')
# Short only
parser.boolean(:debug, desc: 'Debug mode', short: '-d', long: '')
# Long only
parser.string(:config, desc: 'Config file', short: '', long: '--config')
parser.parse(%w[-v 2 -d --config app.yml])parser = SimpleOptions::Options.new(
program_name: 'calculator',
description: 'Simple calculator'
)
parser.integer(:add, desc: 'Add numbers')
parser.integer(:multiply, desc: 'Multiply numbers')
# When -h or --help is specified, displays help without parsing other options
# Even if required options are missing, displays help without error
parser.parse(%w[-h]) # Displays help and exitsDemonstrates the composition pattern:
# examples/todo.rb
require 'simple-cli-options'
parser = SimpleOptions::Options.new(
program_name: 'todo',
description: 'A simple todo list manager'
)
# Concise definition with only name and desc
parser.boolean(:list, desc: 'Show list of todos')
parser.string(:add, desc: 'Add a new todo item')
parser.integer(:delete, desc: 'Delete todo by ID')
parser.parse
if parser.get(:list)
# List display logic
elsif parser.get(:add)
# Add logic
elsif parser.get(:delete)
# Delete logic
endUsage examples:
ruby examples/todo.rb -h # Show help
ruby examples/todo.rb -list # Show todo list
ruby examples/todo.rb -add "Buy milk" # Add new task
ruby examples/todo.rb -delete 3 # Delete task with ID=3# examples/calculator.rb
parser = SimpleOptions::Options.new(
program_name: 'calculator',
description: 'Simple calculator with flexible number support'
)
# Number type supports both integers and floats
parser.number(:add, desc: 'Add two numbers')
parser.number(:multiply, desc: 'Multiply two numbers')
parser.integer(:power, desc: 'Raise to power (integer only)')
parser.parseUsage examples:
ruby examples/calculator.rb -add 3.14 # Float input
ruby examples/calculator.rb -add 42 # Integer input
ruby examples/calculator.rb -multiply 2.5 # Multiplication
ruby examples/calculator.rb -power 2 # Power (integer only)| File | Description |
|---|---|
| examples/todo.rb | Todo list manager. Demonstrates concise definition with name and desc only |
| examples/calculator.rb | Calculator. Shows Number type flexibility (integer/float) |
| examples/paint_calculator.rb | Calculate paint amount from room dimensions |
| examples/greet.rb | Simple greeting tool |
Install dependencies, then run RSpec from the project root:
bundle install
bundle exec rspec spec/test_helper.rb loads RSpec and the library (option.rb, options.rb), so your specs can use Option and Options without requiring them yourself.
- Specs under
spec/: usespec_helper, which already requirestest_helper:# spec/my_feature_spec.rb require_relative 'spec_helper' RSpec.describe Option do it 'does something' do opt = Option.new(:x, short: '-x', long: '--x', desc: 'X') expect(opt.process('value')).to eq 'value' end end
- Specs elsewhere (e.g. in your app): require
test_helperby path so the library and RSpec are loaded:require_relative '/path/to/simple-options/test_helper' # Now Option, Options are available and RSpec is configured
Then run your spec with bundle exec rspec path/to/your_spec.rb.
RuboCop (and rubocop-performance, rubocop-rspec) are in the Gemfile. Run with bundle exec rubocop.
MIT (see LICENSE).