Commands compose. Any command can register child modules as subcommands, producing a tree of arbitrary depth.
Basic nesting
defmodule Devtool.CLI do
use Cheer.Command
command "devtool" do
about "Developer toolkit"
subcommand Devtool.Server
subcommand Devtool.Db
end
end
defmodule Devtool.Server do
use Cheer.Command
command "server" do
about "Server management"
subcommand Devtool.Server.Start
subcommand Devtool.Server.Stop
end
endEach subcommand is a full command module with its own options, arguments, help, and optional children.
Require a subcommand
By default, invoking a parent command with no child shows help. To treat that as an error instead:
command "devtool" do
subcommand_required true
# ...
endAliases
defmodule MyApp.CLI.Checkout do
use Cheer.Command
command "checkout" do
aliases ["co", "ck"]
# ...
end
endmy-app co, my-app ck, and my-app checkout all resolve to the same
module.
Prefix inference
Allow any unambiguous prefix to resolve to a declared subcommand:
command "git" do
infer_subcommands true
subcommand MyApp.CLI.Checkout
subcommand MyApp.CLI.Status
endgit sta -> status
git che -> error: 'che' is ambiguous; candidates: check, checkoutExact matches always win over prefix inference. Aliases are not prefix-matched.
"Did you mean?"
Unknown subcommands produce a typo suggestion:
$ my-app chekout
error: unknown command 'chekout'
Did you mean 'checkout'?
Suggestions are ranked by Jaro distance and gated at 0.7 similarity.
External subcommands (plugins)
Let a command accept unknown subcommand tokens and surface them to
run/2. Enables git-style plugin dispatchers:
command "my-tool" do
external_subcommands true
subcommand MyApp.CLI.Status # declared subs still take precedence
end
@impl Cheer.Command
def run(args, _raw) do
case args[:external_subcommand] do
{name, rest} -> System.cmd("my-tool-#{name}", rest)
nil -> :ok
end
endBehavior:
- Declared subcommands match first.
- First non-option token not matching a declared sub becomes the external subcommand name. Everything after it passes through verbatim, including flags the parent does not know about.
args[:external_subcommand]isnilwhen no external sub was invoked,{name, rest}when one was. Pattern matches are total.- The parent's own options parse before the external subcommand name, including
:num_argsoptions:my-tool --point 1 2 statusgives the parentpoint: [1, 2]and the external sub{"status", []}.
Optional subcommand (run the parent on an unknown token)
By default a command that declares both arguments and subcommands treats an
unrecognized first token as an unknown command and errors. Set
args_conflicts_with_subcommands true to make the subcommand optional: a token
that does not match a declared subcommand is parsed as the parent's own
positional, and option parsing continues across it.
command "roba" do
args_conflicts_with_subcommands true
argument :prompt, type: :string, required: false
subcommand Roba.Commands.History
endBehavior:
roba historydispatches to thehistorysubcommand (declared subs still match first; withinfer_subcommands, an unambiguous prefix matches too).roba "summarize this" --model haikuruns the parent withprompt: "summarize this"andmodel: "haiku".robawith no arguments runs the parent with the optional positional absent.
Unlike external_subcommands, the parent parses normally, so its own options
are recognized rather than passed through.
Propagating version
By default only the root's -V / --version prints its version. To share
with children:
command "my-tool" do
version "1.0.0"
propagate_version true
endHelp for a specific subcommand
All of these show the same help:
my-tool server start --help
my-tool server start -h
my-tool help server startSee also
- Lifecycle hooks for
persistent_before_run, which applies to every descendant. - Help and output for
display_orderon subcommands.