The letter A styled as Alchemists logo. lchemists
Published June 15, 2023 Updated January 28, 2024
Sod Icon

Sod

0.5.0

Sod — as in the ground upon which you stand — provides a Domain Specific Language (DSL) for creating reusable Command Line Interfaces (CLIs). This gem builds upon and enhances native Option Parser behavior by smoothing out the rough edges you wish Option Parser didn’t have.

Features

  • Builds upon and enhances native Option Parser functionality.

  • Provides a simple DSL for composing reusable CLI commands and actions.

  • Provides a blank slate that is fully customizable to your needs.

  • Provides prefabricated commands and actions for quick setup and experimentation.

  • Uses Infusible for function composition.

  • Uses Tone for colorized documentation.

  • Uses Cogger for colorized logging.

Screenshots

DSL

A screenshot of the DSL syntax

Output

A screenshot of the generated help documentation

Requirements

  1. Ruby.

  2. Familiarity with Option Parser syntax and behavior.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install sod --trust-policy HighSecurity

To install without security, run:

gem install sod

You can also add the gem directly to your project:

bundle add sod

Once the gem is installed, you only need to require it:

require "sod"

Usage

Creating and calling a CLI is as simple as:

Sod.new.call
# nil

Granted, the above isn’t terribly exciting — in terms of initial behavior — but illustrates how default behavior provides a blank slate from which to mold custom behavior as you like. To provide minimum functionality, you’ll want to give your CLI a name, banner, and throw in the prefabricated help action:

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on Sod::Prefabs::Actions::Help, self
end

cli.call

# Demo 0.0.0: A demonstration.
#
# USAGE
#   demo [OPTIONS]
#
# OPTIONS
#   -h, --help [COMMAND]     Show this message.

Notice, with only a few extra lines of code, you can build upon the initial blank slate provided for you and start to see your custom CLI take form. You can even take this a step further and outline the structure of your CLI with inline commands:

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on Sod::Prefabs::Actions::Help, self

  on "generate", "Generate project templates."
  on "db", "Manage database."
end

cli.call

# Demo 0.0.0: A demonstration.
#
# USAGE
#   demo [OPTIONS]
#   demo COMMAND [OPTIONS]
#
# OPTIONS
#   -h, --help [COMMAND]     Show this message.
#
# COMMANDS
#   generate                 Generate project templates.
#   db                       Manage database.

We’ll dive into the defaults, prefabrications, and custom commands/actions soon but knowing a help action is provided for you is a good first step in learning how to build your own custom CLI.

Name

A good CLI needs a name and, by default, this is the name of file, script, or IRB session you are currently creating your CLI instance in. For example, when using this project’s bin/console script, my CLI name is:

Sod.new.name  # "console"

The default name is automatically acquired via the $PROGRAM_NAME global variable. Any file extension is immediately trimmed which means creating your CLI instance within a demo.rb file will have a name of "demo". Should this not be desired, you can customize further by providing your own name:

# With a symbol.
Sod.new(:demo).name   # "demo"

# With a string.
Sod.new("demo").name  # "demo"

When using the prefabricated help action, the name of your CLI will also show up in the usage documentation:

Sod.new(:demo) { on Sod::Prefabs::Actions::Help, self }
   .call

# USAGE
#   demo [OPTIONS]
#
# OPTIONS
#   -h, --help [COMMAND]     Show this message.

Banner

The banner is optional but strongly encouraged because it allows you to give your CLI a label and short description. Example:

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on Sod::Prefabs::Actions::Help, self
end

cli.call

# Demo 0.0.0: A demonstration.
#
# USAGE
#   demo [OPTIONS]
#
# OPTIONS
#   -h, --help [COMMAND]     Show this message.

As you can see, when a banner is present, you are able to describe your CLI while providing relevant information such as current version with minimal effort.

DSL

You’ve already seen some of the DSL syntax, via the earlier examples, but now we can zoom in on the building blocks: commands and actions. Only a single method is required to add them: on. For example, here’s what nesting looks like:

Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on "db", "Manage database." do
    on Start
    on Stop

    on "structure", "Manage database structure." do
      on Dump
    end
  end

  on Sod::Prefabs::Actions::Version, "Demo 0.0.0"
  on Sod::Prefabs::Actions::Help, self
end

Despite the Start, Stop, and Dump actions not being implemented yet — because you’ll get a NameError if you try — this does mean you’d eventually have the following functionality available from the command line:

demo db --start
demo db --stop
demo db structure --dump
demo --version
demo --help

The on method is the primary method of the DSL. Short and sweet. You’ll also see on used when implementing custom commands and actions too. The on method can take any number of positional and/or keyword arguments. Here’s an example where you might want to customize your database action by injecting a new dependencies:

Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on DB, "MyDatabase", host: localhost, port: 5432
end

The first positional argument (i.e. DB) is always your action, the second positional argument is the first positional argument to the DB.new method followed by the host and port keyword arguments. In other words, here’s what’s happening:

# Pattern
on DB, *, **

# DSL
on DB, "MyDatabase", host: localhost, port: 5432

# Actual
DB.new "MyDatabase", host: localhost, port: 5432

This also means you get the following benefits:

  • Lazy initialization of your commands/actions.

  • Quick injection of dependencies or customization of dependencies in general.

  • Automatic forwarding of positional and/or keyword arguments to your command/action. Blocks are excluded since they are used by the on method for nesting purposes.

To further understand the DSL, commands, and actions you’ll need to start with actions since they are the building blocks.

Actions

Actions are the lowest building blocks of the DSL which allow you to quickly implement, test, reuse, and compose more complex architectures. They provide a nice layer atop native OptionParser#on functionality.

There are two kinds of actions: custom and prefabricated. We’ll start with custom actions and explore prefabricated actions later. Custom actions allow you to define your own functionality by inheriting from Sod::Action and leveraging the DSL that comes with it.

Macros

Here’s a high level breakdown of the macros you can use:

  • description: Optional (but strongly encouraged). Allows you to describe your action and appears within help documentation. If the description is not defined, then only your action’s handle (i.e. aliases) will be shown.

  • ancillary: Optional. Allows you to provide supplemental text in addition to your description that might be helpful to know about when displaying help documentation. This can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed.

  • on: Required. Allows you to define the behavior of your action through keyword arguments. Otherwise, if not defined, you’ll get a Sod::Error telling you that you must, at a minimum, define some aliases. This macro mimics Option Parser #on behavior via the following positional and keyword arguments:

    • aliases: Required. This is a positional argument and defines the short and long form aliases of your action. Your aliases can be a single string (i.e. on "--version") or an array of short and long form aliases. For example, using on %w[-v --version] would allow you to use -v or --version from the command line to call your action. You can also use boolean aliases such as --build or --[no-]build which the option parser will supply to your #call method as a boolean value.

    • argument: Optional. Serves as documentation, must be a string value, and allows the Option Parser to determine if the argument is required or optional. As per the Option Parser documentation, you could use the following values for example:

      • TEXT: Required text.

      • [TEXT]: Optional text.

      • a,b,c: Required list.

      • [a,b,c]: Optional list.

    • type: Optional. The type is inferred from your argument but, if you need to be explicit or want to use a custom type not supported by default by option parser, you can specify the type by providing a primitive. Example: String, Array, Hash, Date, etc. You can also use custom types, provided by this gem and explained later, or implement your own.

    • allow: Optional. Allows you to define what values are allowed as defined via the argument or type keywords. This can be a string, array, hash, etc. as long as it’s compatible with what is defined via the argument and/or type keyword. This information will also show up in the help documentation as well.

    • default: Optional. Allows you to supply a default value and is a handy for simple values which don’t require lazy evaluation via the corresponding default macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.

    • description: Optional. Allows you to define a description. Handy for short descriptions that can fit on a single line. Otherwise, for longer descriptions, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.

    • ancillary: Optional. Allows you to define ancillary text to supplement your description. It can accept a string or an array. Handy for short, supplementary, text that can fit on a single line. Otherwise, for more verbose details, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.

  • default: Optional. Uses a block which lazy evaluates and resolves your value. This is most helpful when used in combination with an optional argument and/or type which can fallback to a safe default. This information shows up in the help text where the value is rendered as green text. In the case of booleans, they will be rendered as green for true and red for false.

With the above in mind, let’s look at a few examples of what you can do when you put all of this together.

Booleans

Boolean flags are long alases only, take no arguments, and use [no-] syntax after the double dashes. Here’s a minimal implementation:

class Demo < Sod::Action
  on "--[no-]run"

  def call(value) = puts "Got: #{value}"
end

cli = Sod.new { on Demo }

cli.call %w[--run]     # "Got: true"
cli.call %w[--no-run]  # "Got: false"

Because a value is always provided when using a boolean flag, you can make it a required positional parameter via your method definition (i.e. call(value)). You don’t need to worry about type safety because Option Parser will either pass in a true or false value as you can see from the output above.

Arguments

Arguments inform Option Parser how to parse values as either optional or required. Here’s a minimal implementation of an optional argument:

class Demo < Sod::Action
  on %w[-e --echo], argument: "[TEXT]"

  def call(text = nil) = puts "Got: #{text}"
end

cli = Sod.new { on Demo }

cli.call %w[-e]         # "Got: "
cli.call %w[--echo]     # "Got: "
cli.call %w[-e hi]      # "Got: hi"
cli.call %w[--echo hi]  # "Got: hi"

The method definition of call(text = nil) is important because if you call the action directly you’d want to have a safe default that mirrors the on macro. You could provide a non-nil default but we’ll discuss this more later. You could also use a call(text) method definition since Option Parser will always give you a value even if it is nil. You can see see how this behavior plays out in the examples above. On the flip side, when you need a required argument, simply drop the brackets (i.e. []). Here’s an example:

class Demo < Sod::Action
  on %w[-e --echo], argument: "TEXT"

  def call(text) = puts "Got: #{text}"
end

cli = Sod.new { on Demo }

cli.call %w[-e]         # "🛑 Missing argument: -e"
cli.call %w[--echo]     # "🛑 Missing argument: --echo"
cli.call %w[-e hi]      # "Got: hi"
cli.call %w[--echo hi]  # "Got: hi"

There are only three major differences between the earlier optional example and the above required example:

  • The argument is required because it’s not wrapped in brackets.

  • The method definition requires a text parameter.

  • You get an error when not providing an argument.

Types

Types are optional but worth having when you need the safety check. Here’s a minimal example:

class Demo < Sod::Action
  on %w[-e --echo], argument: "NUMBER", type: Float

  def call(number) = puts "Got: #{number}"
end

cli = Sod.new { on Demo }

cli.call %w[--echo 123]   # "Got: 123.0"
cli.call %w[--echo 1.5]   # "Got: 1.5"
cli.call %w[--echo hi]  # 🛑 Invalid argument: --echo hi

Notice the type is a Float where only the first two examples work but the last one ends in an error because Option Parser can’t cast the raw input to a float.

Allows

Allows give you the ability to define what is acceptable as input and need to match your type (if you supply one). Here’s a minimal example:

class Demo < Sod::Action
  on %w[-e --echo], argument: "TEXT", allow: %w[hi hello]

  def call(text) = puts "Got: #{text}"
end

cli = Sod.new { on Demo }

cli.call %w[--echo hi]     # "Got: hi"
cli.call %w[--echo hello]  # "Got: hello"
cli.call %w[--echo test]   # "🛑 Invalid argument: --echo test"

Here you can see the first two examples pass while the last one fails because "test" isn’t a valid value within the allowed array.

Defaults

Defaults are not something that Option Parser supports out-of-the-box but are handy for documentation purposes and within your implementation as fallback values. Here’s a minimal example:

class Demo < Sod::Action
  on %w[-e --echo], argument: "[TEXT]", default: "fallback"

  def call(text = nil) = puts "Got: #{text || default}"
end

cli = Sod.new { on Demo }

cli.call %w[--echo]     # "Got: fallback"
cli.call %w[--echo hi]  # "Got: hi"

Notice how the default is printed when no value is given but is overwritten when an actual value is supplied. This is the correct way to handle defaults but might not be what you are used to. If you’re thinking that you’d rather write the implementation like this:

def call(text = default) = puts "Got: #{text}"

…​you’d not be wrong. In fact, if you initialized and called the action, you’d get what you’d expect:

demo = Demo.new

demo.call       # "Got: fallback"
demo.call "hi"  # "Got: hi"

The reason the above is a problem is because Option Parser ignores optional parameters and all keywords. Here’s the fully modified example:

class Demo < Sod::Action
  on %w[-e --echo], argument: "[TEXT]", default: "fallback"

  def call(text = default) = puts "Got: #{text}"
end

cli = Sod.new { on Demo }

cli.call %w[--echo]     # "Got: "
cli.call %w[--echo hi]  # "Got: hi"

Notice how there is surprising behavior with the first result (i.e. an empty string). This is because when Option Parser completely ignores the value of the optional parameter. I’ve logged an issue if you want to know more. For now, be aware of this quirk as it can be confusing if you are not familiar with Option Parser.

Examples

The following are a few more examples, in case it helps, with the first leveraging all features:

class Echo < Sod::Action
  description "Echo input as output."

  ancillary "Supplementary text.", "Additional text."

  on %w[-e --echo], argument: "[TEXT]", type: String, allow: %w[hello goodbye]

  default { "hello" }

  def call(text = nil) = puts(text || default)
end

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
  on Echo
  on Sod::Prefabs::Actions::Help, self
end

This time, when we run the above implementation, we have additional details:

cli.call

# Demo 0.0.0: A demonstration
#
# USAGE
#   demo [OPTIONS]
#
# OPTIONS
#   -e, --echo [TEXT]        Echo input as output.
#                            Supplementary text.
#                            Additional text.
#                            Use: hello or goodbye.
#                            Default: hello.
#   -h, --help [COMMAND]     Show this message.

cli.call ["--echo"]

# hello

cli.call %w[--echo goodbye]

# goodbye

cli.call %w[--echo hi]

# 🛑 Invalid argument: --echo hi

Notice how the help text is more verbose. Not only do you see the description for the --echo action printed but you also see the two ancillary lines, documentation on what is allowed (i.e. you can only use "hello" or "goodbye"), and what the default will be (i.e. "hello") when --echo doesn’t get an argument since it’s optional. This is why you can see --echo can be called with nothing, an allowed value, or an value that isn’t allowed which causes an invalid argument error to show up.

Lastly, your action’s #call method must be implemented. Otherwise, you’ll get an exception as show here:

class Echo < Sod::Action
  description "Echo input as output."
  on %w[-e --echo]
end

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
  on Echo
  on Sod::Prefabs::Actions::Help, self
end

cli.call ["--echo"]

# `Echo#call [[:rest, :*]]` must be implemented. (NotImplementedError)

At a minimum, your #call method needs to allow the forwarding of positional arguments which means you can use def call(*) if you want to ignore arguments or define which arguments you care about and ignore the rest. Up to you. Also, all of the information defined within your action is available to you within the instance. Here’s an example action which inspects itself:

class Echo < Sod::Action
  description "Echo input as output."

  ancillary "Supplementary."

  on "--inspect", argument: "[TEXT]", type: String, allow: %w[one two], default: "A default."

  def call(*)
    puts handle:, aliases:, argument:, type:, allow:, default:, description:, ancillary:
  end
end

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
  on Echo
  on Sod::Prefabs::Actions::Help, self
end

cli.call ["--inspect"]

# {
#   :handle => "--inspect [TEXT]",
#   :aliases => ["--inspect"],
#   :argument => "[TEXT]",
#   :type => String,
#   :allow => ["one", "two"],
#   :default => "A default.",
#   :description => "Echo input as output.",
#   :ancillary => ["Supplementary."]
# }

Although, not shown in the above, the #to_a and #to_h methods are available as well.

Commands

Commands are a step up from actions in that they allow you to organize and group your actions while giving you the ability to process the data parsed by your actions. If it helps, a command mimics Option Parser behavior when you initialize and define multiple, actionable, blocks. Here’s an example which maps the terminology of this gem with that of Option Parser:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "optparse"

input = {}

# Command
parser = OptionParser.new do |instance|
  # Actions
  instance.on("--[no-]one", "One.") { |value| input[:one] = value }
  instance.on("--[no-]two", "Two.") { |value| input[:two] = value }
end

parser.parse ["--one", "--no-two"]
puts input

# {:one=>true, :two=>false}

The equivalent of the above, as provided by this gem, is:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"
  gem "sod"
end

class One < Sod::Action
  on "--[no-]one", description: "One."

  def call(value) = context.input[:one] = value
end

class Two < Sod::Action
  on "--[no-]two", description: "Two."

  def call(value) = context.input[:two] = value
end

class Demo < Sod::Command
  handle "demo"

  description "A demonstration command."

  on One
  on Two

  def call = puts context.input
end

context = Sod::Context[input: {}]

cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
  on(Demo, context:)
  on Sod::Prefabs::Actions::Help, self
end

cli.call ["demo", "--one", "--no-two"]

# {:one => true, :two => false}

You might be thinking: "Hey, that’s more lines of code!" True but — more importantly — you get the benefit of composible and reusable architectures — because each command/action is encapsulated — which you don’t get with Option Parser. You’ll also notice that the input hash is mutated. The fact that you have to mutate input is a bummer and you should strive to avoid mutation whenever you can. In this case, mutation is necessary because the underlining architecture of the Option Parser doesn’t provide any other way to share state amongst your commands and actions. So this is one example of how you can do that.

As mentioned earlier with actions, commands share a similar DSL with a few differences in terms of macros:

  • handle: Required. The name of your command or the namespace for which you group multiple actions. Must be a string. Otherwise, if not defined, you’ll get a Sod::Error.

  • description: Optional (but strongly recommended). Defines what your command is about and shows up in the help documentation. Otherwise, if not provided, only your command’s handle will be shown.

  • ancillary: Optional. Allows you to provide supplemental text for your description. Can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed below your description.

  • on: Required. The syntax for this is identical to the CLI DSL where you define your action (constant) as the first positional argument followed by any number of positional and/or keyword arguments that you want to feed into your action when the .new method is called.

If we reuse the above example and print the help documentation, you’ll see the following output:

cli.call

# Demo 0.0.0: A demonstration
#
# USAGE
#   demo [OPTIONS]
#   demo COMMAND [OPTIONS]
#
# OPTIONS
#   -h, --help [COMMAND]     Show this message.
#
# COMMANDS
#   demo                     A demonstration command.

…​and if we display help on the demo command itself, we’ll see all of it’s capabilities:

cli.call ["demo"]

# A demonstration command.
#
# USAGE
#   demo [OPTIONS]
#
# OPTIONS
#   --[no-]one
#   --[no-]two

Commands come in two forms: inline and reusable. You’ve already seen how reusable commands work but the next sections will go into more detail.

Inline

Inline commands provide a lightweight way to namespace your actions when you don’t need, or want, to implement a reusable command. If we refactor the earlier example to use inline commands, here’s what it would look like:

cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
  on "demo", "A demonstration command." do
    on One
    on Two
  end

  on Sod::Prefabs::Actions::Help, self
end

Inline commands can have ancillary text by passing in additional arguments after the description. Example:

cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
  on "demo", "A demonstration command.", "Some text.", "Some more text."
end

While the above is convenient, it can get out of control quickly. If this happens, please consider taking your inline command and turning it into a reusable command so your implementation remains organized and readable.

There is no limit on how deep you can go with nesting but if you are using anything beyond one or two levels of nesting then you should reconsider your design as your CLI is getting too complicated.

Reusable

A reusable command is what you saw earlier where you can subclass from Sod::Command to implement your custom command. Here’s the code again:

class Demo < Sod::Command
  handle "demo"

  description "A demonstration command."

  ancillary "Some text.", "Some more text."

  on One
  on Two

  def call = puts "Your implementation goes here."
end

One major difference between reusable and inline commands is that reusable commands allow you implement a #call method. This method is optional, so if you don’t need it, you don’t have to implement it. However, if you do, this means you can process the input from your actions. This method is called after the option parser has parsed all command line input for your actions which gives you a handy way to process all collected input via a single command. 💡 This is how the Rubysmith, Gemsmith, and Hanamismith gems all build new Ruby projects for you based on the actions passed to them via the CLI.

Initialization

In all the action and command examples, thus far, we’ve not used an initializer. You can always customize how your command or action is initialized by defining one and forwarding all keyword arguments to super. Here’s an example for both an action and a command:

class MyAction < Sod::Action
  def initialize(processor: Processor.new, **)
    super(**)
    @processor = processor
  end
end

class MyCommand < Sod::Command
  def initialize(handler: Handler.new, **)
    super(**)
    @handler = handler
  end
end

The reason you need to forward keyword arguments to super is so that injected dependencies from the super class are always available to you. Especially, contexts, which are explained next.

Contexts

Contexts are a mechanism for passing common data between your commands and actions with override capability if desired. They are a hybrid between a Hash and a Struct. They can be constructed two ways depending on your preference:

# Traditional
context = Sod::Context.new defaults_path: "path/to/defaults.yml", version_label: "Demo 0.0.0"

# Short (like Struct or Data)
context = Sod::Context[defaults_path: "path/to/defaults.yml", version_label: "Demo 0.0.0"]

Once you have an instance, you can use it as follows:

# Direct
context.defaults_path               # "path/to/defaults.yml"

# With override.
context["my/path", :defaults_path]  # "my/path"

The override is handy for situations where you have a value (first argument) that you would prefer to use while still being able to fallback to the :defaults_path if the override is nil. When you put all of this together, this means you can build a single context and use it within your commands and actions by injecting it:

context = Sod::Context[defaults_path: "path/to/defaults.yml" version_label: "Demo 0.0.0"]

Sod.new banner: "A demonstration." do
  on(Sod::Prefabs::Commands::Config, context:)
  on(Sod::Prefabs::Actions::Version, context:)
  on Sod::Prefabs::Actions::Help, self
end

💡 When passing a context to a command, it’ll automatically be passed to all actions defined within that command. Each action can then choose to use the context or not.

Types

Types are a way to extend default Option Parser functionality. Here are a few types — not provided by Option Parser — worth knowing about:

Pathname

Provided by this gem and must be manually required since it’s disabled by default. Example:

require "sod"
require "sod/types/pathname"

class Demo < Sod::Action
  on "--path", argument: "PATH", type: Pathname
end

With the above, you’ll always get a Pathname instance as input to your action.

Version

Provided via the Versionaire gem which gives you a Version type when dealing with semantic versions. Here’s how to leverage it:

require "versionaire"
require "versionaire/extensions/option_parser"

class Demo < Sod::Action
  on "--version", argument: "VERSION", type: Versionaire::Version
end

Custom

Creating a custom type requires minimal effort and can be implemented in only a few files:

# lib/my_type.rb

MyType = -> value { # Implementation details go here. }
# lib/extensions/option_parser.rb
require "optparse"

OptionParser.accept(MyType) { |value| MyType.call value }

Once you’ve implemented a custom type, you are then free to require and reference it within the DSL.

Prefabrications

Several pre-built commands and actions are provided for you as foundational tooling to get you up and running quickly. You can use and customize them as desired.

Configure

The configure command — and associated actions — allows you to interact with CLI configurations such as those managed by the XDG, Runcom, and/or Etcher gems which adhere to the XDG Directory Specification. Example:

require "runcom"

context = Sod::Context[
  defaults_path: "defaults.yml",
  xdg_config: Runcom::Config.new("demo/configuration.yml")
]

cli = Sod.new :rubysmith, banner: "Demo 0.0.0: A demonstration." do
  on(Sod::Prefabs::Commands::Config, context:)
  on Sod::Prefabs::Actions::Help, self
end

cli.call ["config"]

# Manage configuration.
#
# USAGE
#   config [OPTIONS]
#
# OPTIONS
#   -c, --create     Create default configuration.
#                    Prompts for local or global path.
#   -e, --edit       Edit project configuration.
#   -v, --view       View project configuration.
#   -d, --delete     Delete project configuration.
#                    Prompts for confirmation.

This action is most useful when building customizable CLIs where you want users of your CLI to have the flexibility of customizing their preferences.

Help

By now you should be familiar with the help action which allows you to print CLI documentation for users of your CLI. This action consumes the entire graph (i.e. self) of information in order to render documentation. You’ll want to add this by default or customize with your own help action should you not like the default functionality. Anything is possible. Here’s some usage:

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on Sod::Prefabs::Actions::Help, self
end

cli.call
cli.call ["-h"]
cli.call ["--help"]
cli.call ["--help", "some_command"]

💡 Passing -h or --help is optional since the CLI will default to printing help if only given a command.

Version

The version action allows users to check which version of your CLI they are using and only requires supplying version information when creating the action:

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on Sod::Prefabs::Actions::Version.new("Demo 0.0.0")
end

cli.call ["-v"]         # Demo 0.0.0
cli.call ["--version"]  # Demo 0.0.0

💡 This pairs well with the Spek gem which pulls this information straight from your gemspec.

Examples

Hopefully the above is plenty of information to get you started but here are a few more examples in case it helps:

Inline Script

The following demonstrates an inline script using commands and actions.

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

# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "sod"
end

class Start < Sod::Action
  include Sod::Import[:logger]

  description "Start database."

  on "--start"

  def call(*) = logger.info { "Starting database..." }
end

class Stop < Sod::Action
  include Sod::Import[:logger]

  description "Stop database."

  on "--stop"

  def call(*) = logger.info { "Stopping database..." }
end

class Echo < Sod::Action
  include Sod::Import[:kernel]

  description "Echo input as output."

  on %w[-e --echo], argument: "TEXT"

  def call(text) = kernel.puts text
end

cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
  on "db", "Manage database." do
    on Start
    on Stop
  end

  on Sod::Prefabs::Actions::Version, "Demo 0.0.0"
  on Sod::Prefabs::Actions::Help, self
end

Once you’ve saved the above to your local disk, you can experiment with it by passing different command line arguments to it:

./demo

# Demo 0.0.0: A demonstration.
#
# USAGE
#   demo [OPTIONS]
#   demo COMMAND [OPTIONS]
#
# OPTIONS
#   -v, --version            Show version.
#   -h, --help [COMMAND]     Show this message.
#
# COMMANDS
#   db                       Manage database.

./demo db

# Manage database.
#
# USAGE
#   db [OPTIONS]
#
# OPTIONS
#   --start     Start database.
#   --stop      Stop database.

./demo db --start
# 🟢 Starting database...

./demo db --stop
# 🟢 Stopping database...

./demo --version
# Demo 0.0.0

Gems

The following gems are built atop Sod and you can study the CLI namespace each or use the Gemsmith gem to generate a CLI template project with all of this baked in for you. Here’s the list:

Development

To contribute, run:

git clone https://github.com/bkuhlmann/sod
cd sod
bin/setup

You can also use the IRB console for direct access to all objects:

bin/console

Architecture

The architecture of this gem is built entirely around Option Parser by using a graph of nodes (i.e. commands) which can be walked since each node within the graph may or may not have children (i.e. nesting).

Architecture Diagram

Tests

To test, run:

bin/rake

Credits