feat(bonfire): Artisan help, examples, JSON listing and error hints
- 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
This commit is contained in:
125
modules/bonfire/help.go
Normal file
125
modules/bonfire/help.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package bonfire
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// helpUsage is the usage line of a command in help and the JSON listing:
|
||||
// the name, its arguments and [options]; passthrough commands end with
|
||||
// [--] <args...> because everything after the name reaches them unparsed.
|
||||
func helpUsage(c Command) string {
|
||||
u := commandUsage(c) + " [options]"
|
||||
if c.Passthrough {
|
||||
u += " [--] <args...>"
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// optionLabel is the left column of a flag: "-x, --name=NAME" for value
|
||||
// flags, "--name" for bare flags, indented four spaces without shorthand.
|
||||
func optionLabel(f Flag) string {
|
||||
label := " "
|
||||
if f.Shorthand != "" {
|
||||
label = "-" + f.Shorthand + ", "
|
||||
}
|
||||
label += "--" + f.Name
|
||||
if !f.Bare {
|
||||
label += "=" + strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
// optionDetail is a flag's description followed by its default and the
|
||||
// repeatable marker.
|
||||
func optionDetail(f Flag) string {
|
||||
parts := []string{}
|
||||
if f.Description != "" {
|
||||
parts = append(parts, f.Description)
|
||||
}
|
||||
if f.Default != "" && !f.Bare {
|
||||
if _, err := strconv.Atoi(f.Default); err == nil {
|
||||
parts = append(parts, "[default: "+f.Default+"]")
|
||||
} else {
|
||||
parts = append(parts, "[default: "+strconv.Quote(f.Default)+"]")
|
||||
}
|
||||
}
|
||||
if f.Repeatable {
|
||||
parts = append(parts, "(multiple values allowed)")
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// renderHelp writes a command's help in the Artisan layout: Description,
|
||||
// Usage, Arguments, Options with defaults, Global options, Help (the long
|
||||
// text) and Examples, each section present only when it has content.
|
||||
func (st *rootState) renderHelp(c Command, g Globals) error {
|
||||
p := DefaultPalette()
|
||||
var b strings.Builder
|
||||
section := func(title string) {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(p.Header.Render(title+":") + "\n")
|
||||
}
|
||||
|
||||
if c.Description != "" {
|
||||
section("Description")
|
||||
b.WriteString(" " + c.Description + "\n")
|
||||
}
|
||||
section("Usage")
|
||||
b.WriteString(" " + helpUsage(c) + "\n")
|
||||
|
||||
width := 0
|
||||
for _, a := range c.Args {
|
||||
width = max(width, len(a.Name))
|
||||
}
|
||||
for _, f := range c.Flags {
|
||||
width = max(width, len(optionLabel(f)))
|
||||
}
|
||||
row := func(label, detail string) {
|
||||
line := " " + p.Success.Render(label)
|
||||
if detail != "" {
|
||||
line += strings.Repeat(" ", width-len(label)+2) + detail
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
if len(c.Args) > 0 {
|
||||
section("Arguments")
|
||||
for _, a := range c.Args {
|
||||
row(a.Name, a.Description)
|
||||
}
|
||||
}
|
||||
if len(c.Flags) > 0 {
|
||||
section("Options")
|
||||
for _, f := range c.Flags {
|
||||
row(optionLabel(f), optionDetail(f))
|
||||
}
|
||||
}
|
||||
section("Global options")
|
||||
writeGlobalOptions(&b, p)
|
||||
if long := strings.TrimSpace(c.Long); long != "" {
|
||||
section("Help")
|
||||
for _, line := range strings.Split(long, "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
b.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
b.WriteString(" " + line + "\n")
|
||||
}
|
||||
}
|
||||
if len(c.Examples) > 0 {
|
||||
section("Examples")
|
||||
bin := usageName(st.name)
|
||||
for i, e := range c.Examples {
|
||||
if i > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if e.Description != "" {
|
||||
b.WriteString(" " + p.Muted.Render("# "+e.Description) + "\n")
|
||||
}
|
||||
b.WriteString(" " + bin + " " + e.Command + "\n")
|
||||
}
|
||||
}
|
||||
return writeStyled(st.out, g, b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user