package wand import ( "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 { 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.version == "" { 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 { 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 } v := reflect.ValueOf(cmd.impl) t := v.Type() t = unpack(t) s := structParameters(t) return len(fields(s...)) > 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 reflect.Value, skip []int) ([]string, []string) { names := docreflect.FunctionParams(v) var types []reflect.Kind for i := 0; i < v.Type().NumIn(); i++ { types = append(types, v.Type().In(i).Kind()) } for _, i := range skip { names = append(names[:i], names[i+1:]...) types = append(types[:i], types[i+1:]...) } 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{} } v := unpack(reflect.ValueOf(cmd.impl)) t := v.Type() p := positionalParameters(t) ior, iow := ioParameters(p) count := len(p) - len(ior) - len(iow) names, types := 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, types: types, variadic: t.IsVariadic(), 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) } 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, typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())), 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) } 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 { hasConfigFromOption := hasConfigFromOption(conf) 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) 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.isHelp, 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 { 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 }