package wand import ( "code.squareroundforest.org/arpio/docreflect" "fmt" "io" "reflect" "sort" "strings" ) const defaultWrap = 112 type ( argumentSet struct { count int names []string variadic bool usesStdin bool usesStdout bool } 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 } ) 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 } } 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 } func constructDoc(cmd Cmd, fullCommand []string) doc { 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 [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 } func generateMarkdown(out io.Writer, cmd Cmd, level int) error { doc := constructDoc(cmd, []string{cmd.name}) return formatMarkdown(out, doc) }