wand/help.go

354 lines
7.4 KiB
Go
Raw 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-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 14:12:18 +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 14:12:18 +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
2025-09-05 03:19:00 +02:00
func help(cmd Cmd) Cmd {
2025-08-18 14:24:31 +02:00
return 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
}
}
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-08-26 03:21:35 +02:00
if !hasHelpCmd && cmd.version == "" {
2025-09-05 03:19:00 +02:00
cmd.subcommands = append(cmd.subcommands, help(cmd))
2025-08-18 14:24:31 +02:00
}
return cmd
}
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-08-24 01:45:25 +02:00
func hasOptions(cmd Cmd) bool {
if cmd.impl == nil {
return false
}
2025-09-01 02:07:48 +02:00
return len(fields(cmd.impl)) > 0
2025-08-24 01:45:25 +02:00
}
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
}
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)
for _, i := range indices {
names = append(names, allNames[i])
2025-08-26 03:21:35 +02:00
}
2025-09-01 02:07:48 +02:00
var types []reflect.Kind
for _, i := range indices {
types = append(types, r.Type().In(i).Kind())
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-09-01 02:07:48 +02:00
p, variadic := positional(cmd.impl)
pi := positionalIndices(cmd.impl)
ior, iow := ioParameters(cmd.impl)
names, types := functionParams(cmd.impl, pi)
if len(names) < len(p) {
2025-08-24 01:45:25 +02:00
names = nil
2025-09-01 02:07:48 +02:00
for i := 0; i < len(p); i++ {
2025-08-24 01:45:25 +02:00
names = append(names, fmt.Sprintf("arg%d", i))
}
}
return argumentSet{
2025-09-01 02:07:48 +02:00
count: len(p),
2025-08-24 04:46:54 +02:00
names: names,
2025-08-26 14:12:18 +02:00
types: types,
2025-09-01 02:07:48 +02:00
variadic: variadic,
2025-08-24 04:46:54 +02:00
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)
}
2025-09-01 02:07:48 +02:00
s := structParameters(cmd.impl)
2025-08-24 01:45:25 +02:00
d := make(map[string]string)
for _, si := range s {
2025-09-01 02:07:48 +02:00
f := structFields(si)
2025-08-24 01:45:25 +02:00
for _, fi := range f {
2025-09-01 02:07:48 +02:00
d[fi.Name()] = docreflect.Field(si, fi.Path()...)
2025-08-24 01:45:25 +02:00
}
}
var o []docOption
f := mapFields(cmd.impl)
for name, fi := range f {
opt := docOption{
name: name,
2025-09-01 02:07:48 +02:00
typ: scalarTypeString(fi[0].Type()),
2025-08-24 01:45:25 +02:00
description: d[name],
shortNames: sf[name],
2025-09-01 02:07:48 +02:00
isBool: fi[0].Type() == bind.Bool,
2025-08-24 01:45:25 +02:00
}
for _, fii := range fi {
2025-09-01 02:07:48 +02:00
if fii.List() {
2025-08-24 01:45:25 +02:00
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 {
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
2025-09-01 02:07:48 +02:00
options := constructOptions(cmd, hasConfigFromOption(conf))
2025-08-26 03:21:35 +02:00
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,
2025-09-05 03:19:00 +02:00
isHelp: cmd.helpFor != nil,
2025-08-26 03:21:35 +02:00
isVersion: cmd.version != "",
2025-08-24 01:45:25 +02:00
hasHelpSubcommand: hasHelpSubcommand(cmd),
2025-09-01 02:07:48 +02:00
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 {
2025-09-05 03:19:00 +02:00
if cmd.helpFor != nil {
cmd = *cmd.helpFor
}
2025-08-26 03:21:35 +02:00
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
}