-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
clino.go
288 lines (257 loc) · 8.04 KB
/
clino.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
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
// Package clino provides a simple way to create CLI (command-line interface) tools.
//
// You can create commands to use with this package by implementing its interfaces.
// It supports the Unix -flag style.
//
// The Command interface contains only a name.
// However, if you try to run a command that doesn't implement any of the
// Runnable, Longer, Parent, or Footer interfaces,
// you are going to get a "missing implementation" error message.
//
// For working with flags, you need to implement the FlagSet interface to a given command.
// If you need global flags, you can do so by defining Program.GlobalFlags.
// You can use it for a -verbose, -config, or other application-wide state flags.
// In example/complex you can see how to use global flags easily.
package clino
import (
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
)
// Command contains the minimal interface for a command: its name (usage).
//
// You usually want to implement the Runnable interface, except for
// help-only commands, or when your command has subcommands (implements Parent).
type Command interface {
Name() string
}
// Shorter description of a command to show in the "help" output on a list of commands.
type Shorter interface {
Short() string
}
// Runnable commands are commands that implement the Run function, and you can run it from the command-line.
// It should receive a context and the command arguments, after parsing any flags.
// A context is required as we want cancelation to be a first-class citizen.
// You can rely on the context for canceling long tasks during tests.
type Runnable interface {
Run(ctx context.Context, args ...string) error
}
// FlagSet you want to use on your command.
// // Flags of the "hello" command.
// func (hc *HelloCommand) Flags(flags *flag.FlagSet) {
// flags.StringVar(&hc.name, "name", "World", "your name")
// }
// You need to implement a Flags function like shown and set any flags you want your commands to parse.
type FlagSet interface {
Flags(flags *flag.FlagSet)
}
// PersistentFlagSet is similar to FlagSet, but flags are inherited by the next commands.
// // PersistentFlags of the "main" command.
// func (mc *MainCommand) PersistentFlags(flags *flag.FlagSet) {
// flags.BoolVar(&hc.verbose, "verbose", false, "verbose mode")
// }
// You need to implement a Flags function like shown and set any flags you want your commands to parse.
type PersistentFlagSet interface {
PersistentFlags(flags *flag.FlagSet)
}
// Longer description or help message for your command.
// The help command prints the returned value of the Long function as the "help" output of a command.
type Longer interface {
Long() string
}
// Footer of a command shown in the "help <command>" output.
// It is useful for things like printing examples.
type Footer interface {
Foot() string
}
// Parent contains all subcommands of a given command.
type Parent interface {
Commands() []Command
}
// Program you want to run.
//
// You should call the Run function, passing the context, root command, and process arguments.
type Program struct {
// Root command is the entrypoint of the program.
Root Command
// GlobalFlags are flags available to all commands.
//
// Deprecated: Use PersistentFlags instead.
GlobalFlags func(flags *flag.FlagSet)
// Output is the default output function to the application.
//
// If not set when calling Run, os.Stdout is set.
// You probably only want to set this for testing.
Output io.Writer
fs *flag.FlagSet
}
// Run program by processing arguments and executing the invoked command.
//
// Context is passed down to the command to simplify testing and cancelation.
// Arguments should be the process arguments (os.Args[1:]...) when you call it from main().
//
// Example:
// p := clino.Program{
// Root: &RootCommand{},
// }
// if err := p.Run(context.Background(), os.Args[1:]...); err != nil {
// fmt.Fprintf(os.Stderr, "%+v\n", err)
// os.Exit(clino.ExitCode(err))
// }
func (p *Program) Run(ctx context.Context, args ...string) error {
if p.Output == nil {
p.Output = os.Stdout
}
if p.Root == nil {
panic("root command not implemented")
}
checkDuplicated(p.Root, []string{p.Root.Name()})
p.fs = flag.NewFlagSet("", flag.ContinueOnError)
p.fs.SetOutput(ioutil.Discard) // skip printing flags -help when parsing flags fail.
if p.GlobalFlags != nil {
p.GlobalFlags(p.fs)
}
return p.runCommand(ctx, args)
}
// checkDuplicated is supposed to be called initially with the root command and check the children implementations, recursively.
func checkDuplicated(cmd Command, trail []string) {
p, ok := cmd.(Parent)
if !ok {
return
}
var m = map[string]struct{}{}
for _, c := range p.Commands() {
name, cmdtrail := c.Name(), append(trail, c.Name())
if _, ok := m[name]; ok {
panic("command implemented multiple times: '" + strings.Join(cmdtrail, " ") + "'")
}
m[name] = struct{}{}
checkDuplicated(c, cmdtrail)
}
}
func isRunnable(cmd Command) bool {
_, ok := cmd.(Runnable)
return ok
}
func commandNotFound(binary string, trail []string) error {
trail = append([]string{binary}, trail...)
return fmt.Errorf("unknown command: '%v'", strings.Join(trail, " "))
}
func (p *Program) loadCommand(ctx context.Context, args []string) []Command {
commands := getSubcommands(p.Root)
cmdArgs := getCommandArgs(args)
return p.walkCommand(commands, cmdArgs)
}
func skipHelpCommand(args []string) []string {
if len(args) != 0 && args[0] == "help" {
return args[1:]
}
return args
}
func (p *Program) runCommand(ctx context.Context, args []string) error {
trail := p.loadCommand(ctx, skipHelpCommand(args))
cmd := trail[len(trail)-1]
for _, c := range trail {
if f, ok := c.(PersistentFlagSet); ok && f != nil {
f.PersistentFlags(p.fs)
}
}
if f, ok := cmd.(FlagSet); ok && f != nil {
f.Flags(p.fs)
}
if (len(args) == 0 && !isRunnable(p.Root)) || (len(args) != 0 && args[0] == "help") {
return p.runHelp(ctx, args)
}
if r, ok := cmd.(Runnable); ok && r != nil {
err := p.fs.Parse(args[len(trail)-1:])
if err == flag.ErrHelp {
return p.runHelp(ctx, args)
}
if err != nil {
return err
}
return r.Run(ctx, p.fs.Args()...)
}
return p.runHelp(ctx, args)
}
func (p *Program) runHelp(ctx context.Context, args []string) error {
if len(args) >= 1 && args[0] == "help" {
args = args[1:]
}
trail := p.walkCommand(getSubcommands(p.Root), getCommandArgs(args))
cmd := trail[len(trail)-1]
var breadcrumb []string
for _, c := range trail {
breadcrumb = append(breadcrumb, c.Name())
}
breadcrumb = breadcrumb[1:]
h := &helper{
Output: p.Output,
Commands: getSubcommands(cmd),
binary: p.Root.Name(),
trail: breadcrumb,
args: args,
fs: p.fs,
}
if l, ok := cmd.(Longer); ok && l != nil {
h.Long = l.Long
}
if f, ok := cmd.(Footer); ok && f != nil {
h.Foot = f.Foot
}
p.setUsableHelp(cmd, h)
return h.Run(ctx)
}
// setUsableHelp is used to only print help for flags and 'usage' message
// if command has subcommands or is runnable.
func (p *Program) setUsableHelp(cmd Command, h *helper) {
_, h.runnable = cmd.(Runnable)
_, parent := cmd.(Parent)
h.usable = h.runnable || parent
}
func getCommand(commands []Command, name string) (cmd Command, ok bool) {
for _, c := range commands {
if name == c.Name() {
return c, true
}
}
return
}
// walkCommand is similar to getCommand, but recursive and it stops
// when it can't find any further command following the path.
// The returned trail value is the "breadcrumb" for the command.
func (p *Program) walkCommand(commands []Command, names []string) (trail []Command) {
trail = append(trail, p.Root)
current := commands
for _, name := range names {
c, next := getCommand(current, name)
if !next {
return
}
trail = append(trail, c)
current = getSubcommands(c)
}
return
}
func getCommandArgs(args []string) (out []string) {
if len(args) == 0 {
return
}
for _, arg := range args {
if strings.HasPrefix(arg, "-") { // stop on first flag
return
}
out = append(out, arg)
}
return
}
func getSubcommands(cmd Command) []Command {
if p, ok := cmd.(Parent); ok && p != nil {
return p.Commands()
}
return []Command{}
}