usage returns a usage string based on the given options, configuration struct, current command, and available commands. It contains [AppAbout], a list of commands and their descriptions, and a list of flags and their descriptions, scoped based on the current command and its associated commands and c
(opts *Options, cfg T, cmd string, cmds ...*Cmd[T])
| 27 | // current command and its associated commands and configuration. |
| 28 | // The resulting string contains color escape codes. |
| 29 | func usage[T any](opts *Options, cfg T, cmd string, cmds ...*Cmd[T]) string { |
| 30 | var b strings.Builder |
| 31 | if cmd == "" { |
| 32 | if opts.AppAbout != "" { |
| 33 | b.WriteString("\n" + opts.AppAbout + "\n\n") |
| 34 | } |
| 35 | } else { |
| 36 | gotCmd := false |
| 37 | for _, c := range cmds { |
| 38 | if c.Name == cmd { |
| 39 | if c.Doc != "" { |
| 40 | b.WriteString("\n" + c.Doc + "\n\n") |
| 41 | } |
| 42 | gotCmd = true |
| 43 | break |
| 44 | } |
| 45 | } |
| 46 | if !gotCmd { |
| 47 | fmt.Println(logx.CmdColor(cmdName()+" help") + logx.ErrorColor(fmt.Sprintf(" failed: command %q not found", cmd))) |
| 48 | os.Exit(1) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | fs := &fields{} |
| 53 | addFields(cfg, fs, cmd) |
| 54 | |
| 55 | cmdName := cmdName() |
| 56 | if cmd != "" { |
| 57 | cmdName += " " + cmd |
| 58 | } |
| 59 | b.WriteString(logx.TitleColor("Usage:\n") + indent + logx.CmdColor(cmdName+" ")) |
| 60 | |
| 61 | posArgStrs := []string{} |
| 62 | |
| 63 | for _, kv := range fs.Order { |
| 64 | v := kv.Value |
| 65 | f := v.Field |
| 66 | |
| 67 | posArgTag, ok := f.Tag.Lookup("posarg") |
| 68 | if ok { |
| 69 | ui := uint64(0) |
| 70 | if posArgTag == "all" || posArgTag == "leftover" { |
| 71 | ui = uint64(len(posArgStrs)) |
| 72 | } else { |
| 73 | var err error |
| 74 | ui, err = strconv.ParseUint(posArgTag, 10, 64) |
| 75 | if err != nil { |
| 76 | slog.Error("programmer error: invalid value for posarg struct tag", "field", f.Name, "posArgTag", posArgTag, "err", err) |
| 77 | } |
| 78 | } |
| 79 | // if the slice isn't big enough, grow it to fit this posarg |
| 80 | if ui >= uint64(len(posArgStrs)) { |
| 81 | posArgStrs = slices.Grow(posArgStrs, len(posArgStrs)-int(ui)+1) // increase capacity |
| 82 | posArgStrs = posArgStrs[:ui+1] // extend to capacity |
| 83 | } |
| 84 | nm := strcase.ToKebab(v.Names[0]) |
| 85 | req, has := f.Tag.Lookup("required") |
| 86 | if req == "+" || req == "true" || !has { // default is required, so !has => required |