wand/help.go
2025-08-26 03:21:35 +02:00

355 lines
7.5 KiB
Go

package wand
import (
"code.squareroundforest.org/arpio/docreflect"
"fmt"
"io"
"reflect"
"sort"
"strings"
"time"
)
type (
argumentSet struct {
count int
names []string
types []string
variadic bool
usesStdin bool
usesStdout bool
minPositional int
maxPositional int
}
synopsis struct {
command string
hasOptions bool
arguments argumentSet
hasSubcommands bool
}
docOption struct {
name string
typ string
description string
shortNames []string
isBool bool
acceptsMultiple bool
}
docConfig struct {
fromOption bool
optional bool
fn string
}
doc struct {
name string
appName string
fullCommand string
synopsis synopsis
description string
hasImplementation bool
isHelp bool
isVersion bool
isDefault bool
hasHelpSubcommand bool
hasHelpOption bool
hasConfigFromOption bool
options []docOption
hasBoolOptions bool
hasListOptions bool
arguments argumentSet
subcommands []doc
configFiles []docConfig
date time.Time
}
)
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
}
}
if !hasHelpCmd && cmd.version == "" {
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 {
if cmd.impl == nil {
return false
}
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
}
}
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
}
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) {
names := docreflect.FunctionParams(v)
var types []reflect.Kind
for i := 0; i < v.Type().NumIn(); i++ {
types = append(types, v.Type().In(i).Kind())
}
for _, i := range skip {
names = append(names[:i], names[i+1:]...)
types = append(types[:i], types[i+1:]...)
}
var stypes []string
for _, t := range types {
stypes = append(stypes, strings.ToLower(fmt.Sprint(t)))
}
return names, stypes
}
func constructArguments(cmd Cmd) argumentSet {
if cmd.impl == nil {
return argumentSet{}
}
v := unpack(reflect.ValueOf(cmd.impl))
t := v.Type()
p := positionalParameters(t)
ior, iow := ioParameters(p)
count := len(p) - len(ior) - len(iow)
names, types := functionParams(v, append(ior, iow...))
if len(names) < count {
names = nil
for i := 0; i < count; i++ {
names = append(names, fmt.Sprintf("arg%d", i))
}
}
return argumentSet{
count: count,
names: names,
types: types,
variadic: t.IsVariadic(),
usesStdin: len(ior) > 0,
usesStdout: len(iow) > 0,
minPositional: cmd.minPositional,
maxPositional: cmd.maxPositional,
}
}
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 {
if cmd.version != "" {
return versionDocs
}
if cmd.impl == nil {
return ""
}
return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
}
func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
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,
typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())),
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)
}
if hasConfigFromOption {
o = append(o, docOption{
name: "config",
description: configOptionDocs,
shortNames: sf["config"],
acceptsMultiple: true,
})
}
return o
}
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
}
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
hasConfigFromOption := hasConfigFromOption(conf)
var subcommands []doc
for _, sc := range cmd.subcommands {
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
}
}
return doc{
name: cmd.name,
appName: fullCommand[0],
fullCommand: strings.Join(fullCommand, " "),
synopsis: constructSynopsis(cmd, fullCommand),
description: constructDescription(cmd),
hasImplementation: cmd.impl != nil,
isDefault: cmd.isDefault,
isHelp: cmd.isHelp,
isVersion: cmd.version != "",
hasHelpSubcommand: hasHelpSubcommand(cmd),
hasHelpOption: hasCustomHelpOption(cmd),
options: options,
hasBoolOptions: hasBoolOptions,
hasListOptions: hasListOptions,
arguments: constructArguments(cmd),
subcommands: subcommands,
configFiles: constructConfigDocs(cmd, conf),
date: time.Now(),
}
}
func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
doc := constructDoc(cmd, conf, fullCommand)
return formatHelp(out, doc)
}
func generateMan(out io.Writer, cmd Cmd, conf Config) error {
doc := constructDoc(cmd, conf, []string{cmd.name})
return formatMan(out, doc)
}
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
doc := constructDoc(cmd, conf, []string{cmd.name})
return formatMarkdown(out, doc, level)
}
func showVersion(out io.Writer, cmd Cmd) error {
_, err := fmt.Fprintln(out, cmd.version)
return err
}