...
1 package ffcli
2
3 import (
4 "context"
5 "errors"
6 "flag"
7 "fmt"
8 "strings"
9 "text/tabwriter"
10
11 "github.com/peterbourgon/ff/v3"
12 )
13
14
15
16
17 type Command struct {
18
19
20
21 Name string
22
23
24
25
26
27
28
29
30
31 ShortUsage string
32
33
34
35
36 ShortHelp string
37
38
39
40
41
42 LongHelp string
43
44
45
46
47
48
49
50 UsageFunc func(c *Command) string
51
52
53
54
55 FlagSet *flag.FlagSet
56
57
58
59 Options []ff.Option
60
61
62 Subcommands []*Command
63
64
65 selected *Command
66 args []string
67
68
69
70
71
72
73
74
75
76
77
78
79
80 Exec func(ctx context.Context, args []string) error
81 }
82
83
84
85
86
87
88
89
90 func (c *Command) Parse(args []string) error {
91 if c.selected != nil {
92 return nil
93 }
94
95 if c.FlagSet == nil {
96 c.FlagSet = flag.NewFlagSet(c.Name, flag.ExitOnError)
97 }
98
99 if c.UsageFunc == nil {
100 c.UsageFunc = DefaultUsageFunc
101 }
102
103 c.FlagSet.Usage = func() {
104 fmt.Fprintln(c.FlagSet.Output(), c.UsageFunc(c))
105 }
106
107 if err := ff.Parse(c.FlagSet, args, c.Options...); err != nil {
108 return err
109 }
110
111 c.args = c.FlagSet.Args()
112 if len(c.args) > 0 {
113 for _, subcommand := range c.Subcommands {
114 if strings.EqualFold(c.args[0], subcommand.Name) {
115 c.selected = subcommand
116 return subcommand.Parse(c.args[1:])
117 }
118 }
119 }
120
121 c.selected = c
122
123 if c.Exec == nil {
124 return NoExecError{Command: c}
125 }
126
127 return nil
128 }
129
130
131
132
133
134
135
136 func (c *Command) Run(ctx context.Context) (err error) {
137 var (
138 unparsed = c.selected == nil
139 terminal = c.selected == c && c.Exec != nil
140 noop = c.selected == c && c.Exec == nil
141 )
142
143 defer func() {
144 if terminal && errors.Is(err, flag.ErrHelp) {
145 c.FlagSet.Usage()
146 }
147 }()
148
149 switch {
150 case unparsed:
151 return ErrUnparsed
152 case terminal:
153 return c.Exec(ctx, c.args)
154 case noop:
155 return NoExecError{Command: c}
156 default:
157 return c.selected.Run(ctx)
158 }
159 }
160
161
162
163
164 func (c *Command) ParseAndRun(ctx context.Context, args []string) error {
165 if err := c.Parse(args); err != nil {
166 return err
167 }
168
169 if err := c.Run(ctx); err != nil {
170 return err
171 }
172
173 return nil
174 }
175
176
177
178
179
180
181 var ErrUnparsed = errors.New("command tree is unparsed, can't run")
182
183
184
185 type NoExecError struct {
186 Command *Command
187 }
188
189
190 func (e NoExecError) Error() string {
191 return fmt.Sprintf("terminal command (%s) doesn't define an Exec function", e.Command.Name)
192 }
193
194
195
196
197
198
199
200 func DefaultUsageFunc(c *Command) string {
201 var b strings.Builder
202
203 fmt.Fprintf(&b, "USAGE\n")
204 if c.ShortUsage != "" {
205 fmt.Fprintf(&b, " %s\n", c.ShortUsage)
206 } else {
207 fmt.Fprintf(&b, " %s\n", c.Name)
208 }
209 fmt.Fprintf(&b, "\n")
210
211 if c.LongHelp != "" {
212 fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
213 }
214
215 if len(c.Subcommands) > 0 {
216 fmt.Fprintf(&b, "SUBCOMMANDS\n")
217 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
218 for _, subcommand := range c.Subcommands {
219 fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
220 }
221 tw.Flush()
222 fmt.Fprintf(&b, "\n")
223 }
224
225 if countFlags(c.FlagSet) > 0 {
226 fmt.Fprintf(&b, "FLAGS\n")
227 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
228 c.FlagSet.VisitAll(func(f *flag.Flag) {
229 space := " "
230 if isBoolFlag(f) {
231 space = "="
232 }
233
234 def := f.DefValue
235 if def == "" {
236 def = "..."
237 }
238
239 fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, f.Usage)
240 })
241 tw.Flush()
242 fmt.Fprintf(&b, "\n")
243 }
244
245 return strings.TrimSpace(b.String()) + "\n"
246 }
247
248 func countFlags(fs *flag.FlagSet) (n int) {
249 fs.VisitAll(func(*flag.Flag) { n++ })
250 return n
251 }
252
253 func isBoolFlag(f *flag.Flag) bool {
254 b, ok := f.Value.(interface {
255 IsBoolFlag() bool
256 })
257 return ok && b.IsBoolFlag()
258 }
259
View as plain text