- 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
224 lines
11 KiB
Markdown
224 lines
11 KiB
Markdown
---
|
|
title: Writing commands
|
|
description: Add console commands to a plugin with bonfire.Command values, arguments, flags, styled output and prompts, and run commands in-process.
|
|
section: console
|
|
order: 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:verb` form, such as `blog:publish`. Plugin commands must use this form; only a few framework commands have bare names.
|
|
- Each `bonfire.Arg` is a positional argument with a name, a description and a `Required` marker. The usage line shows required arguments as `<name>` and optional ones as `[name]`.
|
|
- Each `bonfire.Flag` is a string flag. Set `bonfire.Flag.Bare` for a switch such as `--dry-run` that stores `true` when given alone, and `bonfire.Flag.Repeatable` for a flag that can be given several times.
|
|
- `bonfire.Command.Run` receives the context, a `bonfire.Input` and a `bonfire.Output`.
|
|
- `bonfire.Command.Long` is optional extended help, shown under "Help:" by `help <command>` and `<command> --help`. `bonfire.Command.Examples` and `bonfire.Command.Hidden` are described below. Set `bonfire.Command.Passthrough` for a command that forwards to another program: flag parsing is turned off, so every argument after the command name, including `--help`, `-h` and `--`, reaches `bonfire.Input.Args` unchanged. 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:
|
|
|
|
```go src=modules/bonfire/example_test.go#ExampleCatalog
|
|
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.Warning` and `bonfire.Output.Error` (the last one writes to the error stream);
|
|
- widgets: `bonfire.Output.Table`, `bonfire.Output.Spinner` around a function and `bonfire.Output.Progress` for 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.Choice` and `bonfire.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](#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:
|
|
|
|
```go src=modules/bonfire/example_test.go#ExampleExample
|
|
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:
|
|
|
|
```go src=modules/bonfire/example_test.go#ExampleWithHint
|
|
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:
|
|
|
|
```go src=modules/bonfire/example_test.go#ExampleCheckDocs
|
|
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:
|
|
|
|
```go src=modules/bonfire/example_test.go#ExampleCall
|
|
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:
|
|
|
|
```sh
|
|
./bin/hello greeter:hello
|
|
```
|
|
|
|
That command comes from the greeter plugin of `examples/hello`, whose `Commands` method returns one `bonfire.Command`.
|