wand/help.go

355 lines
7.5 KiB
Go
Raw Normal View History

2025-08-18 14:24:31 +02:00
package wand
import (
2025-08-24 01:45:25 +02:00
"code.squareroundforest.org/arpio/docreflect"
2025-08-18 14:24:31 +02:00
"fmt"
"io"
2025-08-24 01:45:25 +02:00
"reflect"
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
)
type (
2025-08-24 01:45:25 +02:00
argumentSet struct {
2025-08-24 04:46:54 +02:00
count int
names []string
2025-08-26 03:21:35 +02:00
types []string
2025-08-24 04:46:54 +02:00
variadic bool
usesStdin bool
usesStdout bool
minPositional int
maxPositional int
2025-08-24 01:45:25 +02:00
}
2025-08-18 14:24:31 +02:00
2025-08-24 01:45:25 +02:00
synopsis struct {
command string
hasOptions bool
arguments argumentSet
hasSubcommands bool
}
docOption struct {
name string
2025-08-26 03:21:35 +02:00
typ string
2025-08-24 01:45:25 +02:00
description string
shortNames []string
isBool bool
acceptsMultiple bool
}
2025-08-24 04:46:54 +02:00
docConfig struct {
fromOption bool
optional bool
fn string
}
2025-08-24 01:45:25 +02:00
doc struct {
2025-08-24 04:46:54 +02:00
name string
2025-08-26 03:21:35 +02:00
appName string
2025-08-24 04:46:54 +02:00
fullCommand string
synopsis synopsis
description string
hasImplementation bool
isHelp bool
2025-08-26 03:21:35 +02:00
isVersion bool
2025-08-24 04:46:54 +02:00
isDefault bool
hasHelpSubcommand bool
hasHelpOption bool
hasConfigFromOption bool
options []docOption
2025-08-26 03:21:35 +02:00
hasBoolOptions bool
hasListOptions bool
2025-08-24 04:46:54 +02:00
arguments argumentSet
subcommands []doc
configFiles []docConfig
2025-08-26 03:21:35 +02:00
date time.Time
2025-08-24 01:45:25 +02:00
}
)
2025-08-18 14:24:31 +02:00
func help() Cmd {
return Cmd{
name: "help",
isHelp: true,
}
}
func insertHelp(cmd Cmd) Cmd {
var hasHelpCmd bool
for i, sc := range cmd.subcommands {
cmd.subcommands[i] = insertHelp(sc)
if cmd.name == "help" {
hasHelpCmd = true
}
}
2025-08-26 03:21:35 +02:00
if !hasHelpCmd && cmd.version == "" {
2025-08-18 14:24:31 +02:00
cmd.subcommands = append(cmd.subcommands, help())
}
return cmd
}
func hasHelpSubcommand(cmd Cmd) bool {
for _, sc := range cmd.subcommands {
if sc.isHelp {
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-08-24 01:45:25 +02:00
func hasOptions(cmd Cmd) bool {
if cmd.impl == nil {
return false
}
v := reflect.ValueOf(cmd.impl)
t := v.Type()
t = unpack(t)
s := structParameters(t)
return len(fields(s...)) > 0
}
2025-08-26 03:21:35 +02:00
func allCommands(cmd doc) []doc {
commands := []doc{cmd}
for _, sc := range cmd.subcommands {
commands = append(commands, allCommands(sc)...)
}
sort.Slice(commands, func(i, j int) bool {
return commands[i].fullCommand < commands[j].fullCommand
})
return commands
}
func functionParams(v reflect.Value, skip []int) ([]string, []string) {
2025-08-24 01:45:25 +02:00
names := docreflect.FunctionParams(v)
2025-08-26 03:21:35 +02:00
var types []reflect.Kind
for i := 0; i < v.Type().NumIn(); i++ {
types = append(types, v.Type().In(i).Kind())
}
2025-08-24 01:45:25 +02:00
for _, i := range skip {
names = append(names[:i], names[i+1:]...)
2025-08-26 03:21:35 +02:00
types = append(types[:i], types[i+1:]...)
2025-08-24 01:45:25 +02:00
}
2025-08-26 03:21:35 +02:00
var stypes []string
for _, t := range types {
stypes = append(stypes, strings.ToLower(fmt.Sprint(t)))
}
return names, stypes
2025-08-24 01:45:25 +02:00
}
func constructArguments(cmd Cmd) argumentSet {
if cmd.impl == nil {
return argumentSet{}
}
2025-08-26 03:21:35 +02:00
v := unpack(reflect.ValueOf(cmd.impl))
t := v.Type()
2025-08-24 01:45:25 +02:00
p := positionalParameters(t)
ior, iow := ioParameters(p)
count := len(p) - len(ior) - len(iow)
2025-08-26 03:21:35 +02:00
names, types := functionParams(v, append(ior, iow...))
2025-08-24 01:45:25 +02:00
if len(names) < count {
names = nil
for i := 0; i < count; i++ {
names = append(names, fmt.Sprintf("arg%d", i))
}
}
return argumentSet{
2025-08-24 04:46:54 +02:00
count: count,
names: names,
2025-08-26 03:21:35 +02:00
types: types,
2025-08-24 04:46:54 +02:00
variadic: t.IsVariadic(),
usesStdin: len(ior) > 0,
usesStdout: len(iow) > 0,
minPositional: cmd.minPositional,
maxPositional: cmd.maxPositional,
2025-08-24 01:45:25 +02:00
}
}
func constructSynopsis(cmd Cmd, fullCommand []string) synopsis {
return synopsis{
command: strings.Join(fullCommand, " "),
hasOptions: hasOptions(cmd),
arguments: constructArguments(cmd),
hasSubcommands: len(cmd.subcommands) > 0,
}
}
func constructDescription(cmd Cmd) string {
2025-08-26 03:21:35 +02:00
if cmd.version != "" {
return versionDocs
}
2025-08-24 01:45:25 +02:00
if cmd.impl == nil {
return ""
}
return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
}
2025-08-24 04:46:54 +02:00
func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
2025-08-24 01:45:25 +02:00
if cmd.impl == nil {
return nil
}
sf := make(map[string][]string)
for i := 0; i < len(cmd.shortForms); i += 2 {
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
sf[l] = append(sf[l], s)
}
t := unpack(reflect.ValueOf(cmd.impl).Type())
s := structParameters(t)
d := make(map[string]string)
for _, si := range s {
f := fields(si)
for _, fi := range f {
d[fi.name] = docreflect.Field(si, fi.path...)
}
}
var o []docOption
f := mapFields(cmd.impl)
for name, fi := range f {
opt := docOption{
name: name,
2025-08-26 03:21:35 +02:00
typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())),
2025-08-24 01:45:25 +02:00
description: d[name],
shortNames: sf[name],
isBool: fi[0].typ.Kind() == reflect.Bool,
}
for _, fii := range fi {
if fii.acceptsMultiple {
opt.acceptsMultiple = true
}
}
o = append(o, opt)
}
2025-08-24 04:46:54 +02:00
if hasConfigFromOption {
o = append(o, docOption{
name: "config",
description: configOptionDocs,
shortNames: sf["config"],
acceptsMultiple: true,
})
}
2025-08-24 01:45:25 +02:00
return o
}
2025-08-24 04:46:54 +02:00
func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
var docs []docConfig
if conf.file != nil {
docs = append(docs, docConfig{fn: conf.file(cmd).filename, optional: conf.optional})
return docs
}
if conf.fromOption {
docs = append(docs, docConfig{fromOption: true})
return docs
}
for _, m := range conf.merge {
docs = append(docs, constructConfigDocs(cmd, m)...)
}
return docs
}
2025-08-26 03:21:35 +02:00
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
hasConfigFromOption := hasConfigFromOption(conf)
2025-08-24 01:45:25 +02:00
var subcommands []doc
for _, sc := range cmd.subcommands {
2025-08-26 03:21:35 +02:00
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
}
var hasBoolOptions, hasListOptions bool
options := constructOptions(cmd, hasConfigFromOption)
for _, o := range options {
if o.isBool {
hasBoolOptions = true
}
if o.acceptsMultiple {
hasListOptions = true
}
2025-08-24 01:45:25 +02:00
}
return doc{
2025-08-26 03:21:35 +02:00
name: cmd.name,
appName: fullCommand[0],
2025-08-24 01:45:25 +02:00
fullCommand: strings.Join(fullCommand, " "),
synopsis: constructSynopsis(cmd, fullCommand),
description: constructDescription(cmd),
2025-08-24 04:46:54 +02:00
hasImplementation: cmd.impl != nil,
2025-08-24 01:45:25 +02:00
isDefault: cmd.isDefault,
isHelp: cmd.isHelp,
2025-08-26 03:21:35 +02:00
isVersion: cmd.version != "",
2025-08-24 01:45:25 +02:00
hasHelpSubcommand: hasHelpSubcommand(cmd),
hasHelpOption: hasCustomHelpOption(cmd),
2025-08-26 03:21:35 +02:00
options: options,
hasBoolOptions: hasBoolOptions,
hasListOptions: hasListOptions,
2025-08-24 01:45:25 +02:00
arguments: constructArguments(cmd),
subcommands: subcommands,
2025-08-24 04:46:54 +02:00
configFiles: constructConfigDocs(cmd, conf),
2025-08-26 03:21:35 +02:00
date: time.Now(),
2025-08-24 01:45:25 +02:00
}
}
2025-08-26 03:21:35 +02:00
func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
doc := constructDoc(cmd, conf, fullCommand)
2025-08-24 01:45:25 +02:00
return formatHelp(out, doc)
}
2025-08-24 04:46:54 +02:00
func generateMan(out io.Writer, cmd Cmd, conf Config) error {
2025-08-26 03:21:35 +02:00
doc := constructDoc(cmd, conf, []string{cmd.name})
2025-08-24 01:45:25 +02:00
return formatMan(out, doc)
}
2025-08-24 04:46:54 +02:00
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
2025-08-26 03:21:35 +02:00
doc := constructDoc(cmd, conf, []string{cmd.name})
2025-08-24 04:46:54 +02:00
return formatMarkdown(out, doc, level)
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
}