File: command.go

package info (click to toggle)
golang-github-peterbourgon-ff 3.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 408 kB
  • sloc: sh: 9; makefile: 4
file content (285 lines) | stat: -rw-r--r-- 8,007 bytes parent folder | download
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
package ffcli

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"strings"
	"text/tabwriter"

	"github.com/peterbourgon/ff/v3"
)

// Command combines a main function with a flag.FlagSet, and zero or more
// sub-commands. A commandline program can be represented as a declarative tree
// of commands.
type Command struct {
	// Name of the command. Used for sub-command matching, and as a replacement
	// for Usage, if no Usage string is provided. Required for sub-commands,
	// optional for the root command.
	Name string

	// ShortUsage string for this command. Consumed by the DefaultUsageFunc and
	// printed at the top of the help output. Recommended but not required.
	// Should be one line of the form
	//
	//     cmd [flags] subcmd [flags] <required> [<optional> ...]
	//
	// If it's not provided, the DefaultUsageFunc will use Name instead.
	// Optional, but recommended.
	ShortUsage string

	// ShortHelp is printed next to the command name when it appears as a
	// sub-command, in the help output of its parent command. Optional, but
	// recommended.
	ShortHelp string

	// LongHelp is consumed by the DefaultUsageFunc and printed in the help
	// output, after ShortUsage and before flags. Typically a paragraph or more
	// of prose-like text, providing more explicit context and guidance than
	// what is implied by flags and arguments. Optional.
	LongHelp string

	// UsageFunc generates a complete usage output, written to the io.Writer
	// returned by FlagSet.Output() when the -h flag is passed. The function is
	// invoked with its corresponding command, and its output should reflect the
	// command's short usage, short help, and long help strings, subcommands,
	// and available flags. Optional; if not provided, a suitable, compact
	// default is used.
	UsageFunc func(c *Command) string

	// FlagSet associated with this command. Optional, but if none is provided,
	// an empty FlagSet will be defined and attached during the parse phase, so
	// that the -h flag works as expected.
	FlagSet *flag.FlagSet

	// Options provided to ff.Parse when parsing arguments for this command.
	// Optional.
	Options []ff.Option

	// Subcommands accessible underneath (i.e. after) this command. Optional.
	Subcommands []*Command

	// A successful Parse populates these unexported fields.
	selected *Command // the command itself (if terminal) or a subcommand
	args     []string // args that should be passed to Run, if any

	// Exec is invoked if this command has been determined to be the terminal
	// command selected by the arguments provided to Parse or ParseAndRun. The
	// args passed to Exec are the args left over after flags parsing. Optional.
	//
	// If Exec returns flag.ErrHelp, then Run (or ParseAndRun) will behave as if
	// -h were passed and emit the complete usage output.
	//
	// If Exec is nil, and this command is identified as the terminal command,
	// then Parse, Run, and ParseAndRun will all return NoExecError. Callers may
	// check for this error and print e.g. help or usage text to the user, in
	// effect treating some commands as just collections of subcommands, rather
	// than being invocable themselves.
	Exec func(ctx context.Context, args []string) error
}

// Parse the commandline arguments for this command and all sub-commands
// recursively, defining flags along the way. If Parse returns without an error,
// the terminal command has been successfully identified, and may be invoked by
// calling Run.
//
// If the terminal command identified by Parse doesn't define an Exec function,
// then Parse will return NoExecError.
func (c *Command) Parse(args []string) error {
	if c.selected != nil {
		return nil
	}

	if c.FlagSet == nil {
		c.FlagSet = flag.NewFlagSet(c.Name, flag.ExitOnError)
	}

	if c.UsageFunc == nil {
		c.UsageFunc = DefaultUsageFunc
	}

	c.FlagSet.Usage = func() {
		fmt.Fprintln(c.FlagSet.Output(), c.UsageFunc(c))
	}

	if err := ff.Parse(c.FlagSet, args, c.Options...); err != nil {
		return err
	}

	c.args = c.FlagSet.Args()
	if len(c.args) > 0 {
		for _, subcommand := range c.Subcommands {
			if strings.EqualFold(c.args[0], subcommand.Name) {
				c.selected = subcommand
				return subcommand.Parse(c.args[1:])
			}
		}
	}

	c.selected = c

	if c.Exec == nil {
		return NoExecError{Command: c}
	}

	return nil
}

// Run selects the terminal command in a command tree previously identified by a
// successful call to Parse, and calls that command's Exec function with the
// appropriate subset of commandline args.
//
// If the terminal command previously identified by Parse doesn't define an Exec
// function, then Run will return NoExecError.
func (c *Command) Run(ctx context.Context) (err error) {
	var (
		unparsed = c.selected == nil
		terminal = c.selected == c && c.Exec != nil
		noop     = c.selected == c && c.Exec == nil
	)

	defer func() {
		if terminal && errors.Is(err, flag.ErrHelp) {
			c.FlagSet.Usage()
		}
	}()

	switch {
	case unparsed:
		return ErrUnparsed
	case terminal:
		return c.Exec(ctx, c.args)
	case noop:
		return NoExecError{Command: c}
	default:
		return c.selected.Run(ctx)
	}
}

// ParseAndRun is a helper function that calls Parse and then Run in a single
// invocation. It's useful for simple command trees that don't need two-phase
// setup.
func (c *Command) ParseAndRun(ctx context.Context, args []string) error {
	if err := c.Parse(args); err != nil {
		return err
	}

	if err := c.Run(ctx); err != nil {
		return err
	}

	return nil
}

//
//
//

// ErrUnparsed is returned by Run if Parse hasn't been called first.
var ErrUnparsed = errors.New("command tree is unparsed, can't run")

// NoExecError is returned if the terminal command selected during the parse
// phase doesn't define an Exec function.
type NoExecError struct {
	Command *Command
}

// Error implements the error interface.
func (e NoExecError) Error() string {
	return fmt.Sprintf("terminal command (%s) doesn't define an Exec function", e.Command.Name)
}

//
//
//

// DefaultUsageFunc is the default UsageFunc used for all commands
// if no custom UsageFunc is provided.
func DefaultUsageFunc(c *Command) string {
	var b strings.Builder

	if c.ShortHelp != "" {
		fmt.Fprintf(&b, "DESCRIPTION\n")
		fmt.Fprintf(&b, "  %s\n", c.ShortHelp)
		fmt.Fprintf(&b, "\n")
	}

	fmt.Fprintf(&b, "USAGE\n")
	if c.ShortUsage != "" {
		fmt.Fprintf(&b, "  %s\n", c.ShortUsage)
	} else {
		fmt.Fprintf(&b, "  %s\n", c.Name)
	}
	fmt.Fprintf(&b, "\n")

	if c.LongHelp != "" {
		fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
	}

	if len(c.Subcommands) > 0 {
		fmt.Fprintf(&b, "SUBCOMMANDS\n")
		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
		for _, subcommand := range c.Subcommands {
			fmt.Fprintf(tw, "  %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
		}
		tw.Flush()
		fmt.Fprintf(&b, "\n")
	}

	if countFlags(c.FlagSet) > 0 {
		fmt.Fprintf(&b, "FLAGS\n")
		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
		c.FlagSet.VisitAll(func(f *flag.Flag) {
			space := " "
			if isBoolFlag(f) {
				space = "="
			}

			// If the help text contains backticks,
			// e.g. "foo `bar` baz"`, we'll get:
			//
			//   argname = "bar"
			//   usage   = "foo bar baz"
			//
			// Otherwise, it's an educated guess for a placeholder,
			// or an empty string if one couldn't be determined.
			argname, usage := flag.UnquoteUsage(f)

			// For the argument name printed in the help,
			// the order of preference is:
			//
			//  1. the default value
			//  2. the back-quoted name from the help text
			//  3. the '...' placeholder
			var def string
			switch {
			case f.DefValue != "":
				def = f.DefValue
			case argname != "":
				def = argname
			default:
				def = "..."
			}

			fmt.Fprintf(tw, "  -%s%s%s\t%s\n", f.Name, space, def, usage)
		})
		tw.Flush()
		fmt.Fprintf(&b, "\n")
	}

	return strings.TrimSpace(b.String()) + "\n"
}

func countFlags(fs *flag.FlagSet) (n int) {
	fs.VisitAll(func(*flag.Flag) { n++ })
	return n
}

func isBoolFlag(f *flag.Flag) bool {
	b, ok := f.Value.(interface {
		IsBoolFlag() bool
	})
	return ok && b.IsBoolFlag()
}