- help <command> and <command> --help print Description, Usage,
Arguments, Options with defaults, Global options, Help and Examples
- Command.Examples and Command.Hidden (appended, additive); list --all
shows hidden commands, list --format=json prints a bonfire.Listing
- CheckDocs reports commands lacking description, long help, examples or
argument and option descriptions
- Error block with the ERROR badge, WithHint explanation and Try lines
({bin} replaced), cobra suggestions as Try, -v error chain, messages
scrubbed by sunscreen; Reported errors print nothing and exit 1
11 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Writing commands | Add console commands to a plugin with bonfire.Command values, arguments, flags, styled output and prompts, and run commands in-process. | console | 40 |
Writing commands
A plugin adds console commands to the application binary the way a WinterCMS plugin calls registerConsoleCommand. In SummerCMS a command is a plain bonfire.Command value, and the plugin returns its commands from pact.HasCommands. summer make:command acme.blog Publish generates a starting point in console/publish.go.
Defining a command
A bonfire.Command has a name, a description, its positional arguments and flags, and a run function:
- The name is in
namespace:verbform, such asblog:publish. Plugin commands must use this form; only a few framework commands have bare names. - Each
bonfire.Argis a positional argument with a name, a description and aRequiredmarker. The usage line shows required arguments as<name>and optional ones as[name]. - Each
bonfire.Flagis a string flag. Setbonfire.Flag.Barefor a switch such as--dry-runthat storestruewhen given alone, andbonfire.Flag.Repeatablefor a flag that can be given several times. bonfire.Command.Runreceives the context, abonfire.Inputand abonfire.Output.bonfire.Command.Longis optional extended help, shown under "Help:" byhelp <command>and<command> --help.bonfire.Command.Examplesandbonfire.Command.Hiddenare described below. Setbonfire.Command.Passthroughfor a command that forwards to another program: flag parsing is turned off, so every argument after the command name, including--help,-hand--, reachesbonfire.Input.Argsunchanged. Declared flags then appear in the usage text only, and declared arguments are not count-checked.
Read arguments with bonfire.Input.Argument, scalar and bare flags with bonfire.Input.Flag, and repeatable flags with bonfire.Input.Flags, which returns the values in the order given:
publish := bonfire.Command{
Name: "blog:publish",
Description: "Publish a post",
Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}},
Flags: []bonfire.Flag{
{Name: "dry-run", Description: "Report without writing", Bare: true},
{Name: "tag", Description: "Tag to add (repeatable)", Repeatable: true},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
slug, _ := in.Argument("slug")
dryRun, _ := in.Flag("dry-run")
out.Printf("publish %s, tags %v, dry run %s\n", slug, in.Flags("tag"), dryRun)
return nil
},
}
// The generated main publishes a catalog of every command on the app.
catalog := bonfire.NewCatalog([]bonfire.Command{publish})
args := []string{"hello-world", "--tag", "news", "--tag", "go", "--dry-run"}
if err := catalog.Call(context.Background(), "blog:publish", args, os.Stdout); err != nil {
fmt.Println(err)
}
// Output: publish hello-world, tags [news go], dry run true
Registering commands
Return the commands from the plugin's Commands method, which implements pact.HasCommands. The generated main appends every plugin's commands after the framework's runtime commands. Command names must be unique: when two commands share a name, or a command is named list or help, bonfire.NewRoot fails with bonfire.ErrDuplicateCommand naming the command, and the binary does not start. Keep your commands in your plugin's own namespace, such as blog:. The global options are reserved as well: a flag named verbose, quiet, ansi, no-ansi, no-interaction, env or help, or with the shorthand v, q, n or h, makes bonfire.NewRoot fail with bonfire.ErrReservedFlag.
A scaffolded plugin's Commands method returns the generated list of everything in console/, so commands created with summer make:command are registered without editing plugin.go.
Output
bonfire.Output is the console your command writes to. Besides bonfire.Output.Printf and bonfire.Output.Println, it provides:
- status lines:
bonfire.Output.Info,bonfire.Output.Success,bonfire.Output.Warningandbonfire.Output.Error(the last one writes to the error stream); - widgets:
bonfire.Output.Table,bonfire.Output.Spinneraround a function andbonfire.Output.Progressfor a progress bar, which fall back to plain lines when the output is not a terminal; - prompts:
bonfire.Output.Ask,bonfire.Output.Confirm,bonfire.Output.Choiceandbonfire.Output.Secret. Prompts return their defaults when input ends, so a command run from cron or a script never hangs.
Return an error from Run to fail the command. The binary prints it as an error block on stderr and exits with status 1; see Errors and hints.
Examples and hidden commands
bonfire.Command.Examples lists usage examples. Each bonfire.Example holds the command line without the binary and a short description; help prints the description as a # comment and the line after bin/<name> (or summer for the tool's own commands), so one example serves both. bonfire.Command.Hidden keeps a command that still runs out of the list; list --all and list --format=json show it:
export := bonfire.Command{
Name: "blog:export",
Description: "Export published posts as JSON",
Long: "Writes every published post, newest first, to a JSON file.",
Flags: []bonfire.Flag{
{Name: "path", Description: "Output file", Default: "posts.json"},
},
Examples: []bonfire.Example{
{Command: "blog:export --path=backup.json", Description: "Export to another file"},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
return nil
},
}
// Hidden commands run, but list shows them only with --all.
cleanup := bonfire.Command{
Name: "blog:cleanup",
Description: "Remove orphaned uploads",
Hidden: true,
}
root, err := bonfire.NewRootIO("acme", []bonfire.Command{export, cleanup}, strings.NewReader(""), os.Stdout, os.Stdout)
if err != nil {
fmt.Println(err)
return
}
bonfire.Execute(context.Background(), root, []string{"help", "blog:export"})
// Output:
// Description:
// Export published posts as JSON
//
// Usage:
// blog:export [options]
//
// Options:
// --path=PATH Output file [default: "posts.json"]
//
// Global options:
// -h, --help Display help for the given command
// -q, --quiet Do not output any message
// -v|vv|vvv Increase verbosity
// --ansi|no-ansi Force (or disable) ANSI output
// -n, --no-interaction Do not ask any interactive question
// --env The environment the command runs under
//
// Help:
// Writes every published post, newest first, to a JSON file.
//
// Examples:
// # Export to another file
// bin/acme blog:export --path=backup.json
list --format=json prints every command with its arguments, options (with defaults) and examples as a bonfire.Listing, for documentation checks, scripts and agents. bonfire.Describe builds the same listing from a command slice.
Errors and hints
When Run returns an error, the binary prints a block on stderr: the ERROR badge and the message, then "Run with -v for details.". With -v the block lists the wrapped errors under "Caused by:" instead. Messages pass through sunscreen.Scrub, so a token inside an error text is not printed.
Wrap an error with bonfire.WithHint to explain it and to say what to run next. {bin} in the Try text becomes bin/<name> or summer, so the hint names the right program:
publish := bonfire.Command{
Name: "blog:publish",
Description: "Publish a draft post",
Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
slug, _ := in.Argument("slug")
return bonfire.WithHint(fmt.Errorf("blog: no draft named %q", slug),
"Only drafts can be published; published posts keep their slug.",
"{bin} blog:drafts")
},
}
root, err := bonfire.NewRootIO("acme", []bonfire.Command{publish}, strings.NewReader(""), os.Stdout, os.Stdout)
if err != nil {
fmt.Println(err)
return
}
code := bonfire.Execute(context.Background(), root, []string{"blog:publish", "hello-world"})
fmt.Println("exit code:", code)
// Output:
// ERROR blog: no draft named "hello-world"
//
// Only drafts can be published; published posts keep their slug.
//
// Try:
// bin/acme blog:drafts
//
// Run with -v for details.
//
// exit code: 1
A command that has already printed its problems, such as a checker that lists every finding, returns bonfire.Reported(err): the binary exits 1 and prints nothing more. A mistake in how the command was called, such as an unknown flag or a missing argument, exits 2 and the block suggests help <command>; wrap your own usage checks with bonfire.Usage to get the same exit code.
Documentation coverage
bonfire.CheckDocs reports each command that lacks a description, long help, an example, or a description of one of its arguments or options. It only reports; the framework's own test fails on any problem, and a plugin repository can call it from a test and log the problems:
commands := []bonfire.Command{
{
Name: "blog:publish",
Description: "Publish a draft post",
Long: "Publishes the draft with the given slug and notifies subscribers.",
Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}},
Examples: []bonfire.Example{{Command: "blog:publish hello-world"}},
},
{
Name: "blog:reindex",
Description: "Rebuild the search index",
Flags: []bonfire.Flag{{Name: "batch", Default: "500"}},
},
}
for _, problem := range bonfire.CheckDocs(commands) {
fmt.Println(problem)
}
// Output: blog:reindex: missing long help, examples, option --batch description
Calling commands in-process
bonfire.Call runs one command of a slice by name with its arguments and writes the output to any writer, like Artisan::call in Laravel. Tests use it to exercise a command without building a binary:
commands := []bonfire.Command{{
Name: "acme:greet",
Description: "Greet someone by name",
Args: []bonfire.Arg{{Name: "name", Required: true}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
name, _ := in.Argument("name")
out.Printf("Hello, %s\n", name)
return nil
},
}}
if err := bonfire.Call(context.Background(), commands, "acme:greet", []string{"blog"}, os.Stdout); err != nil {
fmt.Println(err)
}
// Output: Hello, blog
The generated main also publishes the application's complete command list as a bonfire.Catalog on the container. Code outside the command line, such as the scheduler, looks it up and calls commands through bonfire.Catalog.Call, and checks for one with bonfire.Catalog.Has.
Running your command
After summer build, your command is part of the binary:
./bin/hello greeter:hello
That command comes from the greeter plugin of examples/hello, whose Commands method returns one bonfire.Command.