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 }