-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcli.go
More file actions
327 lines (298 loc) · 7.9 KB
/
cli.go
File metadata and controls
327 lines (298 loc) · 7.9 KB
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
package main
import (
"flag"
"fmt"
"reflect"
"strings"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/wordwrap"
)
// mode describes a group of related CLI flags and their help presentation.
type mode struct {
name string
description string
usage string // e.g. "run [flags] <task>" — shown before flags
examples []string // e.g. "run -session=<name> -status" — shown before flags
inv any // struct with flag/pos tags; the invocation type
after func() string // if set, rendered after flags
}
// registerFlags walks all modes, reflects over their inv structs, and
// registers each flag with the stdlib flag package. When the same flag
// name appears in multiple modes, it is registered once and all struct
// fields sharing that name are synced after parsing via syncSharedFlags.
func registerFlags(modes []mode) {
registered := map[string]bool{}
for i := range modes {
m := &modes[i]
if m.inv == nil {
continue
}
rv := reflect.ValueOf(m.inv)
if rv.Kind() != reflect.Pointer {
panic(fmt.Sprintf("mode %q inv must be a pointer to a struct", m.name))
}
rv = rv.Elem()
rt := rv.Type()
for j := 0; j < rt.NumField(); j++ {
field := rt.Field(j)
flagName := field.Tag.Get("flag")
if flagName == "" || registered[flagName] {
continue
}
registered[flagName] = true
usage := field.Tag.Get("usage")
defValue := field.Tag.Get("default")
fv := rv.Field(j)
switch field.Type.Kind() {
case reflect.String:
flag.StringVar(fv.Addr().Interface().(*string), flagName, defValue, usage)
case reflect.Bool:
flag.BoolVar(fv.Addr().Interface().(*bool), flagName, false, usage)
case reflect.Slice:
if field.Type.Elem().Kind() == reflect.String {
ptr := fv.Addr().Interface().(*[]string)
flag.Func(flagName, usage, func(s string) error {
*ptr = append(*ptr, s)
return nil
})
}
}
}
}
}
// syncSharedFlags copies flag values to all inv struct fields that share
// the same flag name. Call after flag.Parse().
func syncSharedFlags(modes []mode) {
// Collect the parsed value for each flag name.
values := map[string]string{}
flag.Visit(func(f *flag.Flag) {
values[f.Name] = f.Value.String()
})
// Also collect defaults for unset flags.
flag.VisitAll(func(f *flag.Flag) {
if _, set := values[f.Name]; !set {
values[f.Name] = f.DefValue
}
})
for i := range modes {
m := &modes[i]
if m.inv == nil {
continue
}
rv := reflect.ValueOf(m.inv)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
for j := 0; j < rt.NumField(); j++ {
field := rt.Field(j)
flagName := field.Tag.Get("flag")
if flagName == "" {
continue
}
val, ok := values[flagName]
if !ok {
continue
}
fv := rv.Field(j)
switch field.Type.Kind() {
case reflect.String:
fv.SetString(val)
case reflect.Bool:
fv.SetBool(val == "true")
}
}
}
}
// resolveMode determines which mode the user intended based on the flags
// that were set and whether a positional argument is present. Returns nil
// if no mode matches (caller should show help).
func resolveMode(modes []mode) *mode {
posArg := flag.Arg(0)
// Collect which flags were explicitly set.
setFlags := map[string]bool{}
flag.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
// For each mode, check if the user's input is a valid invocation.
var matches []*mode
for i := range modes {
m := &modes[i]
if matchesMode(m, setFlags, posArg) {
matches = append(matches, m)
}
}
if len(matches) == 1 {
return matches[0]
}
return nil
}
// matchesMode returns true if the given set of flags and positional arg
// form a valid (and complete) invocation for this mode.
func matchesMode(m *mode, setFlags map[string]bool, posArg string) bool {
if m.inv == nil {
return false
}
rv := reflect.ValueOf(m.inv)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
modeFlags := map[string]bool{}
hasRequiredPos := false
hasPos := false
hasRequiredFlag := false
requiredFlagSet := false
for field := range rt.Fields() {
if flagName := field.Tag.Get("flag"); flagName != "" {
modeFlags[flagName] = true
if field.Tag.Get("required") == "true" {
hasRequiredFlag = true
if setFlags[flagName] {
requiredFlagSet = true
}
}
}
if field.Tag.Get("pos") != "" {
hasPos = true
if field.Tag.Get("required") == "true" {
hasRequiredPos = true
}
}
}
// Check that no set flags are outside this mode.
for name := range setFlags {
if !modeFlags[name] {
return false
}
}
// If the mode has a required positional and it's missing, no match.
if hasRequiredPos && posArg == "" {
return false
}
// If there's a positional arg but this mode doesn't accept one, no match.
if posArg != "" && !hasPos {
return false
}
// The mode must have some signal — either a required flag is set,
// a required positional is present, or at least one flag is set.
if hasRequiredFlag {
return requiredFlagSet
}
if hasRequiredPos {
return posArg != ""
}
// For modes where nothing is required (like Info), at least one
// flag must be set.
for name := range setFlags {
if modeFlags[name] {
return true
}
}
return false
}
// renderHelp generates the full help text from the mode definitions.
func renderHelp(modes []mode) string {
var b strings.Builder
b.WriteString("Run executes collections of tasks defined in tasks.toml files.\n")
b.WriteString("For documentation and the latest version, please visit GitHub:\n")
b.WriteString("\n")
b.WriteString(" https://monks.co/run\n")
for _, m := range modes {
b.WriteString("\n")
b.WriteString(renderModeHelp(m))
}
return b.String()
}
// renderModeHelp generates the help section for a single mode.
func renderModeHelp(m mode) string {
var b strings.Builder
fmt.Fprintln(&b, headerStyle.Render(m.name))
if m.description != "" {
b.WriteString(indent.String(wordwrap.String(m.description, 68), 2) + "\n")
b.WriteString("\n")
}
if m.usage != "" {
b.WriteString(" " + m.usage + "\n")
b.WriteString("\n")
}
if len(m.examples) > 0 {
for _, ex := range m.examples {
b.WriteString(" " + ex + "\n")
}
b.WriteString("\n")
}
// Render flags from the inv struct.
if m.inv != nil {
rv := reflect.ValueOf(m.inv)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
for field := range rt.Fields() {
flagName := field.Tag.Get("flag")
if flagName == "" {
continue
}
f := flag.CommandLine.Lookup(flagName)
if f == nil {
continue
}
renderFlagEntry(&b, f)
}
}
if m.after != nil {
b.WriteString("\n")
b.WriteString(m.after())
}
return b.String()
}
// renderFlagEntry renders a single flag in the standard indented format.
func renderFlagEntry(b *strings.Builder, f *flag.Flag) {
fmt.Fprintf(b, " -%s", f.Name)
name, usage := flag.UnquoteUsage(f)
if len(name) > 0 {
b.WriteString("=")
b.WriteString(name)
}
if !isZeroValue(f, f.DefValue) {
fmt.Fprintf(b, " (default %q)", f.DefValue)
}
b.WriteString("\n")
usage = strings.ReplaceAll(usage, "\n", "\n \t")
usage = wordwrap.String(usage, 52)
usage = indent.String(usage, 8)
b.WriteString(usage)
b.WriteString("\n")
}
// isZeroValue determines whether the string represents the zero
// value for a flag.
func isZeroValue(f *flag.Flag, value string) bool {
typ := reflect.TypeOf(f.Value)
var z reflect.Value
if typ.Kind() == reflect.Pointer {
z = reflect.New(typ.Elem())
} else {
z = reflect.Zero(typ)
}
return value == z.Interface().(flag.Value).String()
}
// setPositional sets the positional arg value on the inv struct.
func setPositional(m *mode) {
posArg := flag.Arg(0)
if m.inv == nil || posArg == "" {
return
}
rv := reflect.ValueOf(m.inv)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
for j := 0; j < rt.NumField(); j++ {
field := rt.Field(j)
if field.Tag.Get("pos") != "" {
rv.Field(j).SetString(posArg)
return
}
}
}