wand/help.go

1022 lines
22 KiB
Go
Raw Permalink Normal View History

2025-08-18 14:24:31 +02:00
package wand
import (
2025-09-01 02:07:48 +02:00
"code.squareroundforest.org/arpio/bind"
2025-09-01 04:10:35 +02:00
"code.squareroundforest.org/arpio/docreflect"
2025-12-10 20:31:10 +01:00
. "code.squareroundforest.org/arpio/textfmt"
2025-08-18 14:24:31 +02:00
"fmt"
2025-12-10 20:31:10 +01:00
"github.com/iancoleman/strcase"
"golang.org/x/term"
2025-08-18 14:24:31 +02:00
"io"
2025-12-10 20:31:10 +01:00
"os"
2025-08-24 01:45:25 +02:00
"reflect"
2025-12-10 20:31:10 +01:00
"regexp"
2025-08-26 03:21:35 +02:00
"sort"
2025-08-18 14:24:31 +02:00
"strings"
2025-08-26 03:21:35 +02:00
"time"
2025-08-18 14:24:31 +02:00
)
2025-12-10 20:31:10 +01:00
const (
defaultHelpWidth = 72
minPrintWidth = 42
maxPrintWidth = 360
markdownWrapWidth = 112
)
const (
noOptionHints = iota
commandSpecificHints
allRelevantHints
2025-08-24 01:45:25 +02:00
)
2025-08-18 14:24:31 +02:00
2025-12-10 20:31:10 +01:00
var sentenceDelimiter = regexp.MustCompile("[.?!]")
2025-09-05 03:19:00 +02:00
func help(cmd Cmd) Cmd {
2025-12-10 20:31:10 +01:00
h := Cmd{
2025-09-01 02:07:48 +02:00
name: "help",
2025-09-05 03:19:00 +02:00
helpFor: &cmd,
shortForms: cmd.shortForms,
2025-08-18 14:24:31 +02:00
}
2025-12-10 20:31:10 +01:00
cmd.subcommands = append(cmd.subcommands, h)
return cmd
2025-08-18 14:24:31 +02:00
}
func insertHelp(cmd Cmd) Cmd {
var hasHelpCmd bool
for i, sc := range cmd.subcommands {
2025-09-05 03:19:00 +02:00
if sc.name == "help" {
2025-08-18 14:24:31 +02:00
hasHelpCmd = true
2025-09-05 03:19:00 +02:00
continue
2025-08-18 14:24:31 +02:00
}
2025-09-05 03:19:00 +02:00
cmd.subcommands[i] = insertHelp(sc)
2025-08-18 14:24:31 +02:00
}
2025-12-10 20:31:10 +01:00
if hasHelpCmd || cmd.version != "" {
return cmd
2025-08-18 14:24:31 +02:00
}
2025-12-10 20:31:10 +01:00
return help(cmd)
2025-08-18 14:24:31 +02:00
}
func hasHelpSubcommand(cmd Cmd) bool {
for _, sc := range cmd.subcommands {
2025-09-05 03:19:00 +02:00
if sc.helpFor != nil {
2025-08-18 14:24:31 +02:00
return true
}
}
return false
}
func hasCustomHelpOption(cmd Cmd) bool {
2025-08-26 03:21:35 +02:00
if cmd.impl == nil {
return false
}
2025-08-18 14:24:31 +02:00
mf := mapFields(cmd.impl)
_, has := mf["help"]
return has
}
func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
if hasHelpSubcommand(cmd) {
fmt.Fprintf(out, "Show help:\n%s help\n", strings.Join(fullCommand, " "))
return
}
if !hasCustomHelpOption(cmd) {
fmt.Fprintf(out, "Show help:\n%s --help\n", strings.Join(fullCommand, " "))
return
}
}
2025-12-10 20:31:10 +01:00
func allNonHelpOptions(cmd Cmd) []bind.Field {
var options []bind.Field
if cmd.impl != nil {
f := fields(cmd.impl)
for _, fi := range f {
options = append(options, fi)
}
}
for _, sc := range cmd.subcommands {
options = append(options, allNonHelpOptions(sc)...)
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
return options
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func allConfigFiles(conf Config) []Config {
if conf.file != nil || conf.fromOption {
return []Config{conf}
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
var files []Config
for _, c := range conf.merge {
files = append(files, allConfigFiles(c)...)
}
2025-08-26 03:21:35 +02:00
2025-12-10 20:31:10 +01:00
return files
2025-08-26 03:21:35 +02:00
}
2025-09-01 02:07:48 +02:00
func functionParams(v any, indices []int) ([]string, []string) {
var names []string
r := reflect.ValueOf(v)
allNames := docreflect.FunctionParams(r)
2025-12-10 20:31:10 +01:00
if len(allNames) == r.Type().NumIn() {
for _, i := range indices {
names = append(names, allNames[i])
}
} else {
names = make([]string, len(indices))
}
for i := range names {
names[i] = strings.TrimSpace(names[i])
if names[i] == "" {
names[i] = fmt.Sprintf("arg%d", i+1)
}
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
var types []string
2025-09-01 02:07:48 +02:00
for _, i := range indices {
2025-12-10 20:31:10 +01:00
t := r.Type().In(i)
types = append(types, scalarTypeStringOf(t))
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
return names, types
}
func paragraphs(text string) []string {
l := strings.Split(text, "\n")
for i := range l {
l[i] = strings.TrimSpace(l[i])
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
var (
pl []string
p []string
)
for _, li := range l {
if li == "" && len(pl) > 0 {
p = append(p, strings.Join(pl, " "))
pl = nil
continue
}
if li == "" {
continue
}
pl = append(pl, li)
}
if len(pl) > 0 {
p = append(p, strings.Join(pl, " "))
}
return p
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func firstSentence(text string) string {
s := sentenceDelimiter.Split(text, 2)
s[0] = strings.TrimSpace(s[0])
return s[0]
}
func wrapWidth() int {
width := defaultHelpWidth
fd := int(os.Stdin.Fd())
if !term.IsTerminal(fd) {
return width
}
w, _, err := term.GetSize(fd)
if err != nil {
return width
}
width = w - 8
if width < minPrintWidth {
width = minPrintWidth
}
if width > maxPrintWidth {
width = maxPrintWidth
}
return width
}
func implementationCommand(cmd Cmd) Cmd {
if cmd.impl != nil {
return cmd
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
for _, subCmd := range cmd.subcommands {
if subCmd.isDefault {
return subCmd
2025-08-24 01:45:25 +02:00
}
}
2025-12-10 20:31:10 +01:00
return cmd
}
2026-01-07 21:43:12 +01:00
func trimName(doc, name string) string {
firstWord, rest, found := strings.Cut(doc, " ")
if !found {
return doc
}
firstWord = strings.TrimSpace(firstWord)
if strcase.ToKebab(firstWord) != strcase.ToKebab(name) {
return doc
}
return strings.TrimSpace(rest)
}
2025-12-10 20:31:10 +01:00
func docCommandNameText(cmd Cmd, fullCommand []string) []string {
command := strings.Join(fullCommand, " ")
fdoc := docreflect.Function(reflect.ValueOf(cmd.impl))
fdoc = strings.TrimSpace(fdoc)
2026-01-07 21:43:12 +01:00
fdoc = trimName(fdoc, cmd.name)
2025-12-10 20:31:10 +01:00
fdocp := paragraphs(fdoc)
if len(fdocp) == 0 {
return []string{command}
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
return []string{command, "-", firstSentence(fdocp[0])}
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func docCommandName(cmd Cmd, fullCommand []string) Entry {
s := docCommandNameText(cmd, fullCommand)
txt := make([]Txt, len(s))
for i := range s {
txt[i] = Text(s[i])
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
return Paragraph(Cat(txt...))
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func docSynopsis(cmd Cmd, fullCommand []string, titleLevel, wrapWidth int) []Entry {
implCmd := implementationCommand(cmd)
command := strings.Join(fullCommand, " ")
synopsisTitle := Title(titleLevel, "Synopsis:")
if implCmd.impl == nil {
synopsis := Syntax(Choice(Sequence(Symbol(command), Required(Symbol("subcommand")))))
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
}
pi := positionalIndices(implCmd.impl)
names, types := functionParams(implCmd.impl, pi)
if len(names) == 0 {
synopsis := Syntax(
Choice(
Sequence(Symbol(command), ZeroOrMore(Symbol("options"))),
Sequence(Symbol(command), Required(Symbol("subcommand"))),
),
)
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
}
var args []SyntaxItem
_, variadic := positional(implCmd.impl)
for i := range names[:len(names)-1] {
args = append(args, Required(Sequence(Symbol(names[i]), Symbol(types[i]))))
}
lastArity := Required
switch {
case variadic && implCmd.minPositional >= len(pi):
lastArity = OneOrMore
case variadic:
lastArity = ZeroOrMore
}
last := len(names) - 1
args = append(args, lastArity(Sequence(Symbol(names[last]), Symbol(types[last]))))
var argCountHint Entry
needsArgCountHint := implCmd.impl != nil && (implCmd.minPositional > 0 || implCmd.maxPositional > 0)
if needsArgCountHint {
var hint string
switch {
case implCmd.minPositional > 0 && implCmd.maxPositional > 0:
hint = fmt.Sprintf(
"Expecting min %d and max %d total number of arguments.",
implCmd.minPositional,
implCmd.maxPositional,
)
case implCmd.minPositional > 0:
hint = fmt.Sprintf(
"Expecting min %d total number of arguments.",
implCmd.minPositional,
)
case implCmd.maxPositional > 0:
hint = fmt.Sprintf(
"Expecting max %d total number of arguments.",
implCmd.maxPositional,
)
}
argCountHint = Paragraph(Text(hint))
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
if len(args) > 0 {
args = append([]SyntaxItem{Optional(Symbol("--"))}, args...)
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
synopsis := Syntax(
Choice(
Sequence(append([]SyntaxItem{Symbol(command), ZeroOrMore(Symbol("options"))}, args...)...),
Sequence(Symbol(command), Required(Symbol("subcommand"))),
),
)
if !needsArgCountHint {
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
}
return []Entry{synopsisTitle, Indent(synopsis, 4, 0), Wrap(argCountHint, wrapWidth)}
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func docs(cmd Cmd) []Entry {
implCmd := implementationCommand(cmd)
if implCmd.impl == nil {
return nil
}
fdoc := docreflect.Function(reflect.ValueOf(implCmd.impl))
fdoc = strings.TrimSpace(fdoc)
2026-01-07 21:43:12 +01:00
fdoc = trimName(fdoc, implCmd.name)
2025-12-10 20:31:10 +01:00
fdocp := paragraphs(fdoc)
if len(fdocp) == 0 {
2025-08-24 01:45:25 +02:00
return nil
}
2025-12-10 20:31:10 +01:00
var e []Entry
for _, p := range fdocp {
e = append(e, Paragraph(Text(p)))
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
return e
}
func docOptions(cmd Cmd, conf Config, titleLevel, wrapWidth, hintStyle int) []Entry {
implCmd := implementationCommand(cmd)
optionsTitle := Title(titleLevel, "Options:")
fields := make(map[string][]bind.Field)
docs := make(map[string]string)
structs := structParameters(implCmd.impl)
for _, s := range structs {
f := structFields(s)
2025-08-24 01:45:25 +02:00
for _, fi := range f {
2025-12-10 20:31:10 +01:00
fields[fi.Name()] = append(fields[fi.Name()], fi)
if d := docs[fi.Name()]; d == "" {
2026-01-07 21:43:12 +01:00
fdocs := docreflect.Field(s, fi.Path()...)
fdocs = trimName(fdocs, fi.Name())
docs[fi.Name()] = fdocs
2025-12-10 20:31:10 +01:00
}
}
}
if len(fields) == 0 {
var optionItems []DefinitionItem
if _, ok := fields["config"]; !ok && hasConfigFromOption(conf) {
optionItems = append(
optionItems,
Definition(Text("--config"), Text("Configuration file."), NoBullet()),
)
}
optionItems = append(
optionItems,
Definition(
Text("--help"),
Text("Show help."),
NoBullet(),
),
)
return []Entry{
optionsTitle,
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
2025-08-24 01:45:25 +02:00
}
}
2025-12-10 20:31:10 +01:00
var (
names []string
optionItems []DefinitionItem
hasAcceptsMultiple, hasBoolOptions, hasGrouping bool
)
for n := range fields {
names = append(names, n)
}
sort.Strings(names)
s2l := shortFormsToLong(implCmd.shortForms)
l2s := longFormsToShort(implCmd.shortForms)
for _, name := range names {
var def string
f := fields[name]
sf := l2s[name]
if len(sf) == 0 {
def = fmt.Sprintf("--%s %s", name, scalarTypeString(f[0].Type()))
} else {
sfs := make([]string, len(sf))
copy(sfs, sf)
for i := range sfs {
sfs[i] = fmt.Sprintf("-%s", sfs[i])
}
def = fmt.Sprintf(
"--%s, %s %s",
name,
strings.Join(sfs, ", "),
scalarTypeString(f[0].Type()),
)
}
for _, fi := range f {
if fi.List() {
def = fmt.Sprintf("%s [*]", def)
hasAcceptsMultiple = true
}
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
if f[0].Type() == bind.Bool {
hasBoolOptions = true
if len(s2l) > 1 && len(sf) > 0 {
hasGrouping = true
2025-08-24 01:45:25 +02:00
}
}
2025-12-10 20:31:10 +01:00
optionItems = append(
optionItems,
Definition(Text(def), Text(docs[name]), NoBullet()),
)
}
if _, ok := fields["config"]; !ok && hasConfigFromOption(conf) {
optionItems = append(
optionItems,
Definition(Text("--config"), Text("Configuration file."), NoBullet()),
)
}
var (
helpShortForms []string
helpShortFormsString string
)
_, hasHelpOption := fields["help"]
if !hasHelpOption {
hsf := make([]string, len(l2s["help"]))
copy(hsf, l2s["help"])
sort.Strings(hsf)
helpShortForms = append(helpShortForms, hsf...)
}
if len(helpShortForms) > 0 {
for i := range helpShortForms {
helpShortForms[i] = fmt.Sprintf("-%s", helpShortForms[i])
}
helpShortFormsString = strings.Join(helpShortForms, ", ")
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
if helpShortFormsString != "" {
optionItems = append(
optionItems,
Definition(
Text(fmt.Sprintf("--help, %s", helpShortFormsString)),
Text("Show help."), NoBullet(),
),
)
} else if !hasHelpOption {
optionItems = append(
optionItems,
Definition(Text("--help"), Text("Show help."), NoBullet()),
)
}
if hintStyle == noOptionHints {
return []Entry{
optionsTitle,
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
}
}
switch hintStyle {
case allRelevantHints:
var numShortForm int
allOptions := allNonHelpOptions(cmd)
for _, o := range allOptions {
hasAcceptsMultiple = hasAcceptsMultiple || o.List()
hasBoolOptions = hasBoolOptions || o.Type() == bind.Bool
if _, hasShortForm := l2s[o.Name()]; hasShortForm {
numShortForm++
}
hasGrouping = hasGrouping || o.Type() == bind.Bool && numShortForm > 1
}
2025-08-24 04:46:54 +02:00
}
2025-12-10 20:31:10 +01:00
var hints []string
if hasAcceptsMultiple {
hints = append(hints, docListOptions)
}
if hasBoolOptions {
hints = append(hints, docBoolOptions)
}
if hasGrouping {
hints = append(hints, docOptionGrouping)
}
hints = append(hints, docOptionValues)
if len(hints) == 1 {
return []Entry{
optionsTitle,
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
Wrap(Paragraph(Text(hints[0])), wrapWidth),
}
}
items := make([]ListItem, len(hints))
for i := range hints {
items[i] = Item(Text(hints[i]))
}
return []Entry{
optionsTitle,
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
Wrap(Paragraph(Text("Hints:")), wrapWidth),
Wrap(List(items...), wrapWidth),
}
2025-08-24 01:45:25 +02:00
}
2025-12-30 16:52:10 +01:00
func docListSubcommands(cmd Cmd, fullCommand []string, level, wrapWidth int) []Entry {
subcommandsTitle := Title(level+1, "Subcommands:")
2025-12-10 20:31:10 +01:00
var (
items []DefinitionItem
hasNonHelp bool
)
scs := make([]Cmd, len(cmd.subcommands))
copy(scs, cmd.subcommands)
sort.Slice(scs, func(i, j int) bool {
return scs[i].name < scs[j].name
})
for _, sc := range scs {
var description string
name := sc.name
if sc.helpFor != nil {
description = "Show help."
} else if sc.version != "" {
description = "Show version information."
} else {
if sc.isDefault {
name = fmt.Sprintf("%s (default)", name)
}
hasNonHelp = true
description = docreflect.Function(reflect.ValueOf(sc.impl))
description = strings.TrimSpace(description)
2026-01-07 21:43:12 +01:00
description = trimName(description, sc.name)
2025-12-10 20:31:10 +01:00
descriptionP := paragraphs(description)
if len(descriptionP) > 0 {
description = firstSentence(descriptionP[0])
}
}
items = append(items, Definition(Text(name), Text(description), NoBullet()))
2025-08-24 04:46:54 +02:00
}
2025-12-10 20:31:10 +01:00
if hasNonHelp {
command := strings.Join(fullCommand, " ")
hint := Paragraph(Text(fmt.Sprintf(docSubcommandHelpFmt, command, command)))
return []Entry{
subcommandsTitle,
Wrap(Indent(DefinitionList(items...), 4, 0), wrapWidth),
Wrap(hint, wrapWidth),
}
2025-08-24 04:46:54 +02:00
}
2025-12-10 20:31:10 +01:00
return []Entry{
subcommandsTitle,
Wrap(Indent(DefinitionList(items...), 4, 0), wrapWidth),
2025-08-24 04:46:54 +02:00
}
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
func docFullSubcommands(cmd Cmd, conf Config, level int) []Entry {
2025-12-10 20:31:10 +01:00
var e []Entry
e = append(
e,
2025-12-30 16:52:10 +01:00
Title(level+1, "Subcommands:"),
2025-12-10 20:31:10 +01:00
Paragraph(Text(docSubcommandHint)),
)
type subcommandDef struct {
fullCommand []string
command Cmd
}
var (
allSubcommands []subcommandDef
collectSubcommands func(cmd Cmd) []subcommandDef
)
collectSubcommands = func(cmd Cmd) []subcommandDef {
var defs []subcommandDef
for _, sc := range cmd.subcommands {
if sc.impl != nil || sc.version != "" {
defs = append(defs, subcommandDef{fullCommand: []string{cmd.name, sc.name}, command: sc})
}
scDefs := collectSubcommands(sc)
for i := range scDefs {
scDefs[i].fullCommand = append([]string{cmd.name}, scDefs[i].fullCommand...)
}
defs = append(defs, scDefs...)
}
2025-08-24 04:46:54 +02:00
2025-12-10 20:31:10 +01:00
return defs
}
allSubcommands = collectSubcommands(cmd)
for _, sc := range allSubcommands {
command := strings.Join(sc.fullCommand, " ")
if sc.command.isDefault {
command = fmt.Sprintf("%s (default)", command)
}
2025-12-30 16:52:10 +01:00
e = append(e, Indent(Title(level+2, command), 2, 0))
2025-12-10 20:31:10 +01:00
if sc.command.version != "" {
e = append(e, Indent(Paragraph(Text("Show version.")), 4, 0))
continue
}
synopsis := docSynopsis(sc.command, sc.fullCommand, 3, 0)
for i := range synopsis {
synopsis[i] = Indent(synopsis[i], 4, 0)
}
e = append(e, synopsis...)
if docs := docs(sc.command); len(docs) > 0 {
2025-12-30 16:52:10 +01:00
e = append(e, Indent(Title(level+3, "Description:"), 4, 0))
2025-12-10 20:31:10 +01:00
for i := range docs {
docs[i] = Indent(docs[i], 4, 0)
}
e = append(e, docs...)
}
options := docOptions(sc.command, conf, 3, 0, noOptionHints)
for i := range options {
options[i] = Indent(options[i], 4, 0)
}
e = append(e, options...)
}
return e
2025-08-24 04:46:54 +02:00
}
2025-12-30 16:52:10 +01:00
func docEnv(cmd Cmd, level int) []Entry {
2025-12-10 20:31:10 +01:00
// env will not work if the app has a non-standard name:
if !commandNameExpression.MatchString(cmd.name) {
return nil
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
options := allNonHelpOptions(cmd)
if len(options) == 0 {
return nil
}
2025-12-30 16:52:10 +01:00
e := []Entry{Title(level+1, "Environment variables:")}
2025-12-10 20:31:10 +01:00
p := paragraphs(envDocs)
for _, pi := range p {
e = append(e, Indent(Paragraph(Text(pi)), 4, 0))
}
2025-12-30 16:52:10 +01:00
e = append(e, Indent(Title(level+2, "Example environment variable:"), 4, 0))
2025-12-10 20:31:10 +01:00
option := options[0]
name := strcase.ToScreamingSnake(fmt.Sprintf("%s-%s", cmd.name, option.Name()))
value := "42"
if option.Type() == bind.Bool {
value = "true"
}
e = append(e, Indent(CodeBlock(fmt.Sprintf("%s=%s", name, value)), 4, 0))
return e
}
2025-12-30 16:52:10 +01:00
func docConfig(cmd Cmd, conf Config, level int) []Entry {
2025-12-10 20:31:10 +01:00
options := allNonHelpOptions(cmd)
if len(options) == 0 {
return nil
}
configFiles := allConfigFiles(conf)
if len(configFiles) == 0 {
return nil
}
2025-12-30 16:52:10 +01:00
e := []Entry{Title(level+1, "Configuration:")}
2025-12-10 20:31:10 +01:00
p := paragraphs(configDocs)
for _, pi := range p {
e = append(e, Indent(Paragraph(Text(pi)), 4, 0))
}
var items []ListItem
for _, cf := range configFiles {
if cf.fromOption {
items = append(
items,
Item(Text("zero or more configuration files defined by the --config option")),
)
continue
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
text := cf.file(cmd).filename
if cf.optional {
text = fmt.Sprintf("%s (optional)", text)
2025-08-26 03:21:35 +02:00
}
2025-12-10 20:31:10 +01:00
items = append(items, Item(Text(text)))
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
e = append(
e,
2025-12-30 16:52:10 +01:00
Indent(Title(level+2, "Configuration files:"), 4, 0),
2025-12-10 20:31:10 +01:00
Indent(List(items...), 8, 0),
)
option := options[0]
name := option.Name()
value := "42"
if option.Type() == bind.Bool {
value = "true"
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
exampleCode := fmt.Sprintf(
"# Default value for --%s:\n%s = %s",
name,
strcase.ToSnake(name),
value,
)
e = append(
e,
2025-12-30 16:52:10 +01:00
Indent(Title(level+2, "Example configuration entry:"), 4, 0),
2025-12-10 20:31:10 +01:00
Indent(CodeBlock(exampleCode), 4, 0),
)
discardExample := fmt.Sprintf(
"# Discarding an inherited entry:\n%s",
strcase.ToSnake(name),
)
e = append(
e,
2025-12-30 16:52:10 +01:00
Indent(Title(level+2, "Example for discarding an inherited entry:"), 4, 0),
2025-12-10 20:31:10 +01:00
Indent(CodeBlock(discardExample), 4, 0),
)
return e
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func helpCommandName(cmd Cmd, fullCommand []string) []Entry {
return []Entry{docCommandName(cmd, fullCommand)}
}
func helpSynopsis(cmd Cmd, fullCommand []string, width int) []Entry {
return docSynopsis(cmd, fullCommand, 1, width)
}
func helpDocs(cmd Cmd, width int) []Entry {
paragraphs := docs(cmd)
for i := range paragraphs {
paragraphs[i] = Wrap(paragraphs[i], width)
2025-09-05 03:19:00 +02:00
}
2025-12-10 20:31:10 +01:00
return paragraphs
}
func helpOptions(cmd Cmd, conf Config, width int) []Entry {
return docOptions(cmd, conf, 1, width, commandSpecificHints)
}
func helpSubcommands(cmd Cmd, fullCommand []string, width int) []Entry {
2025-12-30 16:52:10 +01:00
return docListSubcommands(cmd, fullCommand, 0, width)
2025-12-10 20:31:10 +01:00
}
func manTitle(cmd Cmd, date time.Time, version string) []Entry {
2025-12-30 16:52:10 +01:00
if version == "" {
for _, sc := range cmd.subcommands {
if sc.version != "" {
version = sc.version
break
}
}
}
2025-12-10 20:31:10 +01:00
return []Entry{
Title(
0,
cmd.name,
ManCategory("User Commands"),
ManSection(1),
ReleaseDate(date),
ReleaseVersion(version),
),
}
}
func manCommandName(cmd Cmd) []Entry {
title := Title(1, "Name:")
name := docCommandName(cmd, []string{cmd.name})
name = Indent(name, 4, 0)
return []Entry{title, name}
}
func manSynopsis(cmd Cmd) []Entry {
return docSynopsis(cmd, []string{cmd.name}, 1, 0)
}
func manDocs(cmd Cmd) []Entry {
paragraphs := docs(cmd)
if len(paragraphs) == 0 {
return nil
}
for i := range paragraphs {
paragraphs[i] = Indent(paragraphs[i], 4, 0)
}
return append([]Entry{Title(1, "Description:")}, paragraphs...)
}
func manOptions(cmd Cmd, conf Config) []Entry {
e := docOptions(cmd, conf, 1, 0, allRelevantHints)
for i := 1; i < len(e); i++ {
e[i] = Indent(e[i], 4, 0)
}
return e
}
func manSubcommands(cmd Cmd, conf Config) []Entry {
var hasSubcommands bool
for _, sc := range cmd.subcommands {
if sc.helpFor != nil || sc.version != "" {
continue
}
hasSubcommands = true
break
}
if hasSubcommands {
2025-12-30 16:52:10 +01:00
return docFullSubcommands(cmd, conf, 0)
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
return docListSubcommands(cmd, []string{cmd.name}, 0, 0)
2025-12-10 20:31:10 +01:00
}
func manEnv(cmd Cmd) []Entry {
2025-12-30 16:52:10 +01:00
return docEnv(cmd, 0)
2025-12-10 20:31:10 +01:00
}
func manConfig(cmd Cmd, conf Config) []Entry {
2025-12-30 16:52:10 +01:00
return docConfig(cmd, conf, 0)
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
func markdownTitle(cmd Cmd, level int) []Entry {
2025-12-10 20:31:10 +01:00
txt := docCommandNameText(cmd, []string{cmd.name})
2025-12-30 16:52:10 +01:00
return []Entry{Title(level, strings.Join(txt, " "))}
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
func markdownSynopsis(cmd Cmd, level int) []Entry {
return docSynopsis(cmd, []string{cmd.name}, level+1, markdownWrapWidth)
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
func markdownDocs(cmd Cmd, level int) []Entry {
2025-12-10 20:31:10 +01:00
paragraphs := docs(cmd)
if len(paragraphs) == 0 {
return nil
}
for i := range paragraphs {
paragraphs[i] = Wrap(paragraphs[i], markdownWrapWidth)
}
2025-12-30 16:52:10 +01:00
return append([]Entry{Title(level+1, "Description:")}, paragraphs...)
2025-12-10 20:31:10 +01:00
}
2025-12-30 16:52:10 +01:00
func markdownOptions(cmd Cmd, conf Config, level int) []Entry {
e := docOptions(cmd, conf, level+1, 0, allRelevantHints)
2025-12-10 20:31:10 +01:00
for i := 1; i < len(e); i++ {
e[i] = Wrap(e[i], markdownWrapWidth)
}
return e
}
2025-12-30 16:52:10 +01:00
func markdownSubcommands(cmd Cmd, conf Config, level int) []Entry {
2025-12-10 20:31:10 +01:00
var hasSubcommands bool
for _, sc := range cmd.subcommands {
if sc.helpFor != nil || sc.version != "" {
continue
}
hasSubcommands = true
break
}
var e []Entry
if hasSubcommands {
2025-12-30 16:52:10 +01:00
e = docFullSubcommands(cmd, conf, level)
2025-12-10 20:31:10 +01:00
} else {
2025-12-30 16:52:10 +01:00
e = docListSubcommands(cmd, []string{cmd.name}, level, 0)
2025-12-10 20:31:10 +01:00
}
for i := range e {
e[i] = Wrap(e[i], markdownWrapWidth)
}
return e
}
2025-12-30 16:52:10 +01:00
func markdownEnv(cmd Cmd, level int) []Entry {
e := docEnv(cmd, level)
2025-12-10 20:31:10 +01:00
for i := range e {
e[i] = Wrap(e[i], markdownWrapWidth)
}
return e
}
2025-12-30 16:52:10 +01:00
func markdownConfig(cmd Cmd, conf Config, level int) []Entry {
e := docConfig(cmd, conf, level)
2025-12-10 20:31:10 +01:00
for i := range e {
e[i] = Wrap(e[i], markdownWrapWidth)
}
return e
}
func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
var e []Entry
width := wrapWidth()
e = append(e, helpCommandName(cmd, fullCommand)...)
e = append(e, helpSynopsis(cmd, fullCommand, width)...)
e = append(e, helpDocs(cmd, width)...)
e = append(e, helpOptions(cmd, conf, width)...)
e = append(e, helpSubcommands(cmd, fullCommand, width)...)
return Teletype(out, Doc(e...))
2025-08-24 01:45:25 +02:00
}
2025-12-10 20:31:10 +01:00
func generateMan(out io.Writer, cmd Cmd, conf Config, date time.Time, version string) error {
var e []Entry
e = append(e, manTitle(cmd, date, version)...)
e = append(e, manCommandName(cmd)...)
e = append(e, manSynopsis(cmd)...)
e = append(e, manDocs(cmd)...)
e = append(e, manOptions(cmd, conf)...)
e = append(e, manSubcommands(cmd, conf)...)
e = append(e, manEnv(cmd)...)
e = append(e, manConfig(cmd, conf)...)
return Runoff(out, Doc(e...))
2025-08-24 01:45:25 +02:00
}
2025-08-24 04:46:54 +02:00
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
2025-12-10 20:31:10 +01:00
var e []Entry
2025-12-30 16:52:10 +01:00
e = append(e, markdownTitle(cmd, level)...)
e = append(e, markdownSynopsis(cmd, level)...)
e = append(e, markdownDocs(cmd, level)...)
e = append(e, markdownOptions(cmd, conf, level)...)
e = append(e, markdownSubcommands(cmd, conf, level)...)
e = append(e, markdownEnv(cmd, level)...)
e = append(e, markdownConfig(cmd, conf, level)...)
2025-12-10 20:31:10 +01:00
return Markdown(out, Doc(e...))
2025-08-18 14:24:31 +02:00
}
2025-08-26 03:21:35 +02:00
func showVersion(out io.Writer, cmd Cmd) error {
_, err := fmt.Fprintln(out, cmd.version)
return err
}