354 lines
7.4 KiB
Go
354 lines
7.4 KiB
Go
package wand
|
|
|
|
import (
|
|
"code.squareroundforest.org/arpio/bind"
|
|
"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 Cmd) Cmd {
|
|
return Cmd{
|
|
name: "help",
|
|
helpFor: &cmd,
|
|
shortForms: cmd.shortForms,
|
|
}
|
|
}
|
|
|
|
func insertHelp(cmd Cmd) Cmd {
|
|
var hasHelpCmd bool
|
|
for i, sc := range cmd.subcommands {
|
|
if sc.name == "help" {
|
|
hasHelpCmd = true
|
|
continue
|
|
}
|
|
|
|
cmd.subcommands[i] = insertHelp(sc)
|
|
}
|
|
|
|
if !hasHelpCmd && cmd.version == "" {
|
|
cmd.subcommands = append(cmd.subcommands, help(cmd))
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func hasHelpSubcommand(cmd Cmd) bool {
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.helpFor != nil {
|
|
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
|
|
}
|
|
|
|
return len(fields(cmd.impl)) > 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 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])
|
|
}
|
|
|
|
var types []reflect.Kind
|
|
for _, i := range indices {
|
|
types = append(types, r.Type().In(i).Kind())
|
|
}
|
|
|
|
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{}
|
|
}
|
|
|
|
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) {
|
|
names = nil
|
|
for i := 0; i < len(p); i++ {
|
|
names = append(names, fmt.Sprintf("arg%d", i))
|
|
}
|
|
}
|
|
|
|
return argumentSet{
|
|
count: len(p),
|
|
names: names,
|
|
types: types,
|
|
variadic: variadic,
|
|
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)
|
|
}
|
|
|
|
s := structParameters(cmd.impl)
|
|
d := make(map[string]string)
|
|
for _, si := range s {
|
|
f := structFields(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: scalarTypeString(fi[0].Type()),
|
|
description: d[name],
|
|
shortNames: sf[name],
|
|
isBool: fi[0].Type() == bind.Bool,
|
|
}
|
|
|
|
for _, fii := range fi {
|
|
if fii.List() {
|
|
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 {
|
|
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(conf))
|
|
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.helpFor != nil,
|
|
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 {
|
|
if cmd.helpFor != nil {
|
|
cmd = *cmd.helpFor
|
|
}
|
|
|
|
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
|
|
}
|