wand/help.go

424 lines
8.2 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"
"sort"
2025-08-18 14:24:31 +02:00
"strings"
)
2025-08-24 01:45:25 +02:00
const defaultWrap = 112
2025-08-18 14:24:31 +02:00
type (
2025-08-24 01:45:25 +02:00
argumentSet struct {
count int
names []string
variadic bool
usesStdin bool
usesStdout bool
}
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
description string
shortNames []string
isBool bool
acceptsMultiple bool
}
doc struct {
name string
fullCommand string
synopsis synopsis
description string
isHelp bool
isDefault bool
hasHelpSubcommand bool
hasHelpOption bool
options []docOption
arguments argumentSet
subcommands []doc
}
)
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
}
}
if !hasHelpCmd {
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 {
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
}
func functionParams(v reflect.Value, skip []int) []string {
names := docreflect.FunctionParams(v)
for _, i := range skip {
names = append(names[:i], names[i+1:]...)
}
return names
}
func constructArguments(cmd Cmd) argumentSet {
if cmd.impl == nil {
return argumentSet{}
}
v := reflect.ValueOf(cmd.impl)
t := unpack(v.Type())
p := positionalParameters(t)
ior, iow := ioParameters(p)
count := len(p) - len(ior) - len(iow)
names := 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,
variadic: t.IsVariadic(),
usesStdin: len(ior) > 0,
usesStdout: len(iow) > 0,
}
}
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.impl == nil {
return ""
}
return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
}
func constructOptions(cmd Cmd) []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,
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)
}
return o
}
2025-08-18 14:24:31 +02:00
func constructDoc(cmd Cmd, fullCommand []string) doc {
2025-08-24 01:45:25 +02:00
var subcommands []doc
for _, sc := range cmd.subcommands {
subcommands = append(subcommands, constructDoc(sc, append(fullCommand, sc.name)))
}
return doc{
name: fullCommand[len(fullCommand)-1],
fullCommand: strings.Join(fullCommand, " "),
synopsis: constructSynopsis(cmd, fullCommand),
description: constructDescription(cmd),
isDefault: cmd.isDefault,
isHelp: cmd.isHelp,
hasHelpSubcommand: hasHelpSubcommand(cmd),
hasHelpOption: hasCustomHelpOption(cmd),
options: constructOptions(cmd),
arguments: constructArguments(cmd),
subcommands: subcommands,
}
}
func formatHelp(w io.Writer, doc doc) error {
var err error
println := func(a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintln(w, a...)
}
printf := func(f string, a ...any) {
if err != nil {
return
}
_, err = fmt.Fprintf(w, f, a...)
}
printf(doc.fullCommand)
println()
println()
printf("Synopsis")
println()
println()
printf(doc.synopsis.command)
if doc.synopsis.hasOptions {
printf(" [options ...]")
}
for _, n := range doc.synopsis.arguments.names {
printf(" %s", n)
}
if doc.synopsis.arguments.variadic {
printf("...")
}
println()
if doc.synopsis.hasSubcommands {
printf("%s <subcommand> [options or args...]", doc.synopsis.command)
println()
println()
printf("(For the details about the available subcommands, see the according section below.)")
}
if len(doc.description) > 0 {
println()
println()
printf(doc.description)
}
if len(doc.options) > 0 {
println()
println()
printf("Options")
println()
println()
printf("[*]: accepts multiple instances of the same option")
println()
printf("[b]: booelan flag, true or false, or no argument means true")
println()
var names []string
od := make(map[string]string)
for _, o := range doc.options {
ons := []string{fmt.Sprintf("--%s", o.name)}
for _, sn := range o.shortNames {
ons = append(ons, fmt.Sprintf("-%s", sn))
}
n := strings.Join(ons, ", ")
if o.acceptsMultiple {
n = fmt.Sprintf("%s [*]", n)
}
if o.isBool {
n = fmt.Sprintf("%s [b]", n)
}
names = append(names, n)
od[n] = o.description
}
sort.Strings(names)
var max int
for _, n := range names {
if len(n) > max {
max = len(n)
}
}
for i := range names {
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
names[i] = fmt.Sprintf("%s%s", names[i], pad)
}
for _, n := range names {
println()
printf(n)
if od[n] != "" {
printf(": %s", od[n])
}
}
}
if len(doc.subcommands) > 0 {
println()
println()
printf("Subcommands")
println()
var names []string
cd := make(map[string]string)
for _, sc := range doc.subcommands {
name := sc.name
if sc.isDefault {
name = fmt.Sprintf("%s (default)", name)
}
d := sc.description
if sc.isHelp {
d = fmt.Sprintf("Show this help. %s", d)
}
if sc.hasHelpSubcommand {
d = fmt.Sprintf("%s - For help, see: %s %s help", d, doc.name, sc.name)
} else if sc.hasHelpOption {
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, doc.name, sc.name)
}
cd[name] = d
}
sort.Strings(names)
var max int
for _, n := range names {
if len(n) > max {
max = len(n)
}
}
for i := range names {
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
names[i] = fmt.Sprintf("%s%s", names[i], pad)
}
for _, n := range names {
println()
printf(n)
if cd[n] != "" {
printf(": %s", cd[n])
}
}
}
println()
return err
}
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) error {
doc := constructDoc(cmd, fullCommand)
return formatHelp(out, doc)
}
func formatMan(out io.Writer, doc doc) error {
// if no subcommands, then similar to help
// otherwise:
// title
// all commands
return nil
}
func generateMan(out io.Writer, cmd Cmd) error {
doc := constructDoc(cmd, []string{cmd.name})
return formatMan(out, doc)
}
func formatMarkdown(out io.Writer, doc doc) error {
// if no subcommands, then similar to help
// otherwise:
// title
// all commands
return nil
2025-08-18 14:24:31 +02:00
}
2025-08-24 01:45:25 +02:00
func generateMarkdown(out io.Writer, cmd Cmd, level int) error {
doc := constructDoc(cmd, []string{cmd.name})
return formatMarkdown(out, doc)
2025-08-18 14:24:31 +02:00
}