-
Notifications
You must be signed in to change notification settings - Fork 70
/
usage.go
201 lines (177 loc) · 6.72 KB
/
usage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (c) 2023, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"fmt"
"log/slog"
"os"
"slices"
"strconv"
"strings"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/types"
)
// Indent is the value used for indentation in [Usage].
var Indent = " "
// 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 configuration.
// The resulting string contains color escape codes.
func Usage[T any](opts *Options, cfg T, cmd string, cmds ...*Cmd[T]) string {
var b strings.Builder
if cmd == "" {
if opts.AppAbout != "" {
b.WriteString("\n" + opts.AppAbout + "\n\n")
}
} else {
gotCmd := false
for _, c := range cmds {
if c.Name == cmd {
if c.Doc != "" {
b.WriteString("\n" + c.Doc + "\n\n")
}
gotCmd = true
break
}
}
if !gotCmd {
fmt.Println(logx.CmdColor(CmdName()+" help") + logx.ErrorColor(fmt.Sprintf(" failed: command %q not found", cmd)))
os.Exit(1)
}
}
fields := &Fields{}
AddFields(cfg, fields, cmd)
cmdName := CmdName()
if cmd != "" {
cmdName += " " + cmd
}
b.WriteString(logx.TitleColor("Usage:\n") + Indent + logx.CmdColor(cmdName+" "))
posArgStrs := []string{}
for _, kv := range fields.Order {
v := kv.Value
f := v.Field
posArgTag, ok := f.Tag.Lookup("posarg")
if ok {
ui, err := strconv.ParseUint(posArgTag, 10, 64)
if err != nil {
slog.Error("programmer error: invalid value for posarg struct tag", "field", f.Name, "posArgTag", posArgTag, "err", err)
}
// if the slice isn't big enough, grow it to fit this posarg
if ui >= uint64(len(posArgStrs)) {
posArgStrs = slices.Grow(posArgStrs, len(posArgStrs)-int(ui)+1) // increase capacity
posArgStrs = posArgStrs[:ui+1] // extend to capacity
}
nm := strcase.ToKebab(v.Names[0])
req, has := f.Tag.Lookup("required")
if req == "+" || req == "true" || !has { // default is required, so !has => required
posArgStrs[ui] = logx.CmdColor("<" + nm + ">")
} else {
posArgStrs[ui] = logx.SuccessColor("[" + nm + "]")
}
}
}
b.WriteString(strings.Join(posArgStrs, " "))
if len(posArgStrs) > 0 {
b.WriteString(" ")
}
b.WriteString(logx.SuccessColor("[flags]\n"))
CommandUsage(&b, cmdName, cmd, cmds...)
b.WriteString(logx.TitleColor("\nFlags:\n") + Indent + logx.TitleColor("Flags are case-insensitive, can be in kebab-case, snake_case,\n"))
b.WriteString(Indent + logx.TitleColor("or CamelCase, and can have one or two leading dashes. Use a\n"))
b.WriteString(Indent + logx.TitleColor("\"no\" prefix to turn off a bool flag.\n\n"))
// add meta ones (help, config, verbose, etc) first
mcfields := &Fields{}
AddMetaConfigFields(mcfields)
FlagUsage(mcfields, &b)
FlagUsage(fields, &b)
return b.String()
}
// CommandUsage adds the command usage info for the given commands to the
// given [strings.Builder]. Typically, end-user code should use [Usage] instead.
// It also takes the full name of our command as it appears in the terminal (cmdName),
// (eg: "core build"), and the name of the command we are running (eg: "build").
//
// To be a command that is included in the usage, we must be one command
// nesting depth (subcommand) deeper than the current command (ie, if we
// are on "x", we can see usage for commands of the form "x y"), and all
// of our commands must be consistent with the current command. For example,
// "" could generate usage for "help", "build", and "run", and "mod" could
// generate usage for "mod init", "mod tidy", and "mod edit". This ensures
// that only relevant commands are shown in the usage.
func CommandUsage[T any](b *strings.Builder, cmdName string, cmd string, cmds ...*Cmd[T]) {
acmds := []*Cmd[T]{} // actual commands we care about
var rcmd *Cmd[T] // root command
cmdstrs := strings.Fields(cmd) // subcommand strings in passed command
// need this label so that we can continue outer loop when we have non-matching cmdstr
outer:
for _, c := range cmds {
cstrs := strings.Fields(c.Name) // subcommand strings in command we are checking
if len(cstrs) != len(cmdstrs)+1 { // we must be one deeper
continue
}
for i, cmdstr := range cmdstrs {
if cmdstr != cstrs[i] { // every subcommand so far must match
continue outer
}
}
if c.Root {
rcmd = c
} else if c.Name != cmd { // if it is the same subcommand we are already on, we handle it above in main Usage
acmds = append(acmds, c)
}
}
if len(acmds) != 0 {
b.WriteString(Indent + logx.CmdColor(cmdName+" <subcommand> ") + logx.SuccessColor("[flags]\n"))
}
if rcmd != nil {
b.WriteString(logx.TitleColor("\nDefault command:\n"))
b.WriteString(Indent + logx.CmdColor(rcmd.Name) + "\n" + Indent + Indent + strings.ReplaceAll(rcmd.Doc, "\n", "\n"+Indent+Indent) + "\n") // need to put two indents on every newline for formatting
}
if len(acmds) == 0 && cmd != "" { // nothing to do
return
}
b.WriteString(logx.TitleColor("\nSubcommands:\n"))
// if we are in root, we also add help
if cmd == "" {
b.WriteString(Indent + logx.CmdColor("help") + "\n" + Indent + Indent + "Help shows usage information for a command\n")
}
for _, c := range acmds {
b.WriteString(Indent + logx.CmdColor(c.Name))
if c.Doc != "" {
// we only want the first paragraph of text for subcommand usage; after that is where more specific details can go
doc, _, _ := strings.Cut(c.Doc, "\n\n")
b.WriteString("\n" + Indent + Indent + strings.ReplaceAll(doc, "\n", "\n"+Indent+Indent)) // need to put two indents on every newline for formatting
}
b.WriteString("\n")
}
}
// FlagUsage adds the flag usage info for the given fields
// to the given [strings.Builder]. Typically, end-user code
// should use [Usage] instead.
func FlagUsage(fields *Fields, b *strings.Builder) {
for _, kv := range fields.Order {
f := kv.Value
b.WriteString(Indent)
for i, name := range f.Names {
b.WriteString(logx.CmdColor("-" + strcase.ToKebab(name)))
if i != len(f.Names)-1 {
b.WriteString(", ")
}
}
b.WriteString(" " + logx.SuccessColor(f.Field.Type.String()))
b.WriteString("\n")
field := types.GetField(f.Struct, f.Field.Name)
if field != nil {
b.WriteString(Indent + Indent + strings.ReplaceAll(field.Doc, "\n", "\n"+Indent+Indent)) // need to put two indents on every newline for formatting
}
def, ok := f.Field.Tag.Lookup("default")
if ok && def != "" {
b.WriteString(fmt.Sprintf(" (default: %s)", def))
}
b.WriteString("\n")
}
}