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
|
|
|
}
|