1/*
2Copyright 2016 Google Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package subcommands implements a simple way for a single command to have many
18// subcommands, each of which takes arguments and so forth.
19package subcommands
20
21import (
22	"context"
23	"flag"
24	"fmt"
25	"io"
26	"os"
27	"path"
28	"sort"
29	"strings"
30)
31
32// A Command represents a single command.
33type Command interface {
34	// Name returns the name of the command.
35	Name() string
36
37	// Synopsis returns a short string (less than one line) describing the command.
38	Synopsis() string
39
40	// Usage returns a long string explaining the command and giving usage
41	// information.
42	Usage() string
43
44	// SetFlags adds the flags for this command to the specified set.
45	SetFlags(*flag.FlagSet)
46
47	// Execute executes the command and returns an ExitStatus.
48	Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) ExitStatus
49}
50
51// A Commander represents a set of commands.
52type Commander struct {
53	commands  []*commandGroup
54	topFlags  *flag.FlagSet // top-level flags
55	important []string      // important top-level flags
56	name      string        // normally path.Base(os.Args[0])
57
58	Output io.Writer // Output specifies where the commander should write its output (default: os.Stdout).
59	Error  io.Writer // Error specifies where the commander should write its error (default: os.Stderr).
60}
61
62// A commandGroup represents a set of commands about a common topic.
63type commandGroup struct {
64	name     string
65	commands []Command
66}
67
68// An ExitStatus represents a Posix exit status that a subcommand
69// expects to be returned to the shell.
70type ExitStatus int
71
72const (
73	ExitSuccess ExitStatus = iota
74	ExitFailure
75	ExitUsageError
76)
77
78// NewCommander returns a new commander with the specified top-level
79// flags and command name. The Usage function for the topLevelFlags
80// will be set as well.
81func NewCommander(topLevelFlags *flag.FlagSet, name string) *Commander {
82	cdr := &Commander{
83		topFlags: topLevelFlags,
84		name:     name,
85		Output:   os.Stdout,
86		Error:    os.Stderr,
87	}
88	topLevelFlags.Usage = func() { cdr.explain(cdr.Error) }
89	return cdr
90}
91
92// Register adds a subcommand to the supported subcommands in the
93// specified group. (Help output is sorted and arranged by group name.)
94// The empty string is an acceptable group name; such subcommands are
95// explained first before named groups.
96func (cdr *Commander) Register(cmd Command, group string) {
97	for _, g := range cdr.commands {
98		if g.name == group {
99			g.commands = append(g.commands, cmd)
100			return
101		}
102	}
103	cdr.commands = append(cdr.commands, &commandGroup{
104		name:     group,
105		commands: []Command{cmd},
106	})
107}
108
109// ImportantFlag marks a top-level flag as important, which means it
110// will be printed out as part of the output of an ordinary "help"
111// subcommand.  (All flags, important or not, are printed by the
112// "flags" subcommand.)
113func (cdr *Commander) ImportantFlag(name string) {
114	cdr.important = append(cdr.important, name)
115}
116
117// Execute should be called once the top-level-flags on a Commander
118// have been initialized. It finds the correct subcommand and executes
119// it, and returns an ExitStatus with the result. On a usage error, an
120// appropriate message is printed to os.Stderr, and ExitUsageError is
121// returned. The additional args are provided as-is to the Execute method
122// of the selected Command.
123func (cdr *Commander) Execute(ctx context.Context, args ...interface{}) ExitStatus {
124	if cdr.topFlags.NArg() < 1 {
125		cdr.topFlags.Usage()
126		return ExitUsageError
127	}
128
129	name := cdr.topFlags.Arg(0)
130
131	for _, group := range cdr.commands {
132		for _, cmd := range group.commands {
133			if name != cmd.Name() {
134				continue
135			}
136			f := flag.NewFlagSet(name, flag.ContinueOnError)
137			f.Usage = func() { explain(cdr.Error, cmd) }
138			cmd.SetFlags(f)
139			if f.Parse(cdr.topFlags.Args()[1:]) != nil {
140				return ExitUsageError
141			}
142			return cmd.Execute(ctx, f, args...)
143		}
144	}
145
146	// Cannot find this command.
147	cdr.topFlags.Usage()
148	return ExitUsageError
149}
150
151// Sorting of a slice of command groups.
152type byGroupName []*commandGroup
153
154func (p byGroupName) Len() int           { return len(p) }
155func (p byGroupName) Less(i, j int) bool { return p[i].name < p[j].name }
156func (p byGroupName) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
157
158// explain prints a brief description of all the subcommands and the
159// important top-level flags.
160func (cdr *Commander) explain(w io.Writer) {
161	fmt.Fprintf(w, "Usage: %s <flags> <subcommand> <subcommand args>\n\n", cdr.name)
162	sort.Sort(byGroupName(cdr.commands))
163	for _, group := range cdr.commands {
164		explainGroup(w, group)
165	}
166	if cdr.topFlags == nil {
167		fmt.Fprintln(w, "\nNo top level flags.")
168		return
169	}
170	if len(cdr.important) == 0 {
171		fmt.Fprintf(w, "\nUse \"%s flags\" for a list of top-level flags\n", cdr.name)
172		return
173	}
174
175	fmt.Fprintf(w, "\nTop-level flags (use \"%s flags\" for a full list):\n", cdr.name)
176	for _, name := range cdr.important {
177		f := cdr.topFlags.Lookup(name)
178		if f == nil {
179			panic(fmt.Sprintf("Important flag (%s) is not defined", name))
180		}
181		fmt.Fprintf(w, "  -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
182	}
183}
184
185// Sorting of the commands within a group.
186func (g commandGroup) Len() int           { return len(g.commands) }
187func (g commandGroup) Less(i, j int) bool { return g.commands[i].Name() < g.commands[j].Name() }
188func (g commandGroup) Swap(i, j int)      { g.commands[i], g.commands[j] = g.commands[j], g.commands[i] }
189
190// explainGroup explains all the subcommands for a particular group.
191func explainGroup(w io.Writer, group *commandGroup) {
192	if len(group.commands) == 0 {
193		return
194	}
195	if group.name == "" {
196		fmt.Fprintf(w, "Subcommands:\n")
197	} else {
198		fmt.Fprintf(w, "Subcommands for %s:\n", group.name)
199	}
200	sort.Sort(group)
201
202	aliases := make(map[string][]string)
203	for _, cmd := range group.commands {
204		if alias, ok := cmd.(*aliaser); ok {
205			root := dealias(alias).Name()
206
207			if _, ok := aliases[root]; !ok {
208				aliases[root] = []string{}
209			}
210			aliases[root] = append(aliases[root], alias.Name())
211		}
212	}
213
214	for _, cmd := range group.commands {
215		if _, ok := cmd.(*aliaser); ok {
216			continue
217		}
218
219		name := cmd.Name()
220		names := []string{name}
221
222		if a, ok := aliases[name]; ok {
223			names = append(names, a...)
224		}
225
226		fmt.Fprintf(w, "\t%-15s  %s\n", strings.Join(names, ", "), cmd.Synopsis())
227	}
228	fmt.Fprintln(w)
229}
230
231// explainCmd prints a brief description of a single command.
232func explain(w io.Writer, cmd Command) {
233	fmt.Fprintf(w, "%s", cmd.Usage())
234	subflags := flag.NewFlagSet(cmd.Name(), flag.PanicOnError)
235	subflags.SetOutput(w)
236	cmd.SetFlags(subflags)
237	subflags.PrintDefaults()
238}
239
240// A helper is a Command implementing a "help" command for
241// a given Commander.
242type helper Commander
243
244func (h *helper) Name() string           { return "help" }
245func (h *helper) Synopsis() string       { return "describe subcommands and their syntax" }
246func (h *helper) SetFlags(*flag.FlagSet) {}
247func (h *helper) Usage() string {
248	return `help [<subcommand>]:
249	With an argument, prints detailed information on the use of
250	the specified subcommand. With no argument, print a list of
251	all commands and a brief description of each.
252`
253}
254func (h *helper) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) ExitStatus {
255	switch f.NArg() {
256	case 0:
257		(*Commander)(h).explain(h.Output)
258		return ExitSuccess
259
260	case 1:
261		for _, group := range h.commands {
262			for _, cmd := range group.commands {
263				if f.Arg(0) != cmd.Name() {
264					continue
265				}
266				explain(h.Output, cmd)
267				return ExitSuccess
268			}
269		}
270		fmt.Fprintf(h.Error, "Subcommand %s not understood\n", f.Arg(0))
271	}
272
273	f.Usage()
274	return ExitUsageError
275}
276
277// HelpCommand returns a Command which implements a "help" subcommand.
278func (cdr *Commander) HelpCommand() Command {
279	return (*helper)(cdr)
280}
281
282// A flagger is a Command implementing a "flags" command for a given Commander.
283type flagger Commander
284
285func (flg *flagger) Name() string           { return "flags" }
286func (flg *flagger) Synopsis() string       { return "describe all known top-level flags" }
287func (flg *flagger) SetFlags(*flag.FlagSet) {}
288func (flg *flagger) Usage() string {
289	return `flags [<subcommand>]:
290	With an argument, print all flags of <subcommand>. Else,
291	print a description of all known top-level flags.  (The basic
292	help information only discusses the most generally important
293	top-level flags.)
294`
295}
296func (flg *flagger) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) ExitStatus {
297	if f.NArg() > 1 {
298		f.Usage()
299		return ExitUsageError
300	}
301
302	if f.NArg() == 0 {
303		if flg.topFlags == nil {
304			fmt.Fprintln(flg.Output, "No top-level flags are defined.")
305		} else {
306			flg.topFlags.PrintDefaults()
307		}
308		return ExitSuccess
309	}
310
311	for _, group := range flg.commands {
312		for _, cmd := range group.commands {
313			if f.Arg(0) != cmd.Name() {
314				continue
315			}
316			subflags := flag.NewFlagSet(cmd.Name(), flag.PanicOnError)
317			subflags.SetOutput(flg.Output)
318			cmd.SetFlags(subflags)
319			subflags.PrintDefaults()
320			return ExitSuccess
321		}
322	}
323	fmt.Fprintf(flg.Error, "Subcommand %s not understood\n", f.Arg(0))
324	return ExitFailure
325}
326
327// FlagsCommand returns a Command which implements a "flags" subcommand.
328func (cdr *Commander) FlagsCommand() Command {
329	return (*flagger)(cdr)
330}
331
332// A lister is a Command implementing a "commands" command for a given Commander.
333type lister Commander
334
335func (l *lister) Name() string           { return "commands" }
336func (l *lister) Synopsis() string       { return "list all command names" }
337func (l *lister) SetFlags(*flag.FlagSet) {}
338func (l *lister) Usage() string {
339	return `commands:
340	Print a list of all commands.
341`
342}
343func (l *lister) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) ExitStatus {
344	if f.NArg() != 0 {
345		f.Usage()
346		return ExitUsageError
347	}
348
349	for _, group := range l.commands {
350		for _, cmd := range group.commands {
351			fmt.Fprintf(l.Output, "%s\n", cmd.Name())
352		}
353	}
354	return ExitSuccess
355}
356
357// CommandsCommand returns Command which implements a "commands" subcommand.
358func (cdr *Commander) CommandsCommand() Command {
359	return (*lister)(cdr)
360}
361
362// An aliaser is a Command wrapping another Command but returning a
363// different name as its alias.
364type aliaser struct {
365	alias string
366	Command
367}
368
369func (a *aliaser) Name() string { return a.alias }
370
371// Alias returns a Command alias which implements a "commands" subcommand.
372func Alias(alias string, cmd Command) Command {
373	return &aliaser{alias, cmd}
374}
375
376// dealias recursivly dealiases a command until a non-aliased command
377// is reached.
378func dealias(cmd Command) Command {
379	if alias, ok := cmd.(*aliaser); ok {
380		return dealias(alias.Command)
381	}
382
383	return cmd
384}
385
386// DefaultCommander is the default commander using flag.CommandLine for flags
387// and os.Args[0] for the command name.
388var DefaultCommander *Commander
389
390func init() {
391	DefaultCommander = NewCommander(flag.CommandLine, path.Base(os.Args[0]))
392}
393
394// Register adds a subcommand to the supported subcommands in the
395// specified group. (Help output is sorted and arranged by group
396// name.)  The empty string is an acceptable group name; such
397// subcommands are explained first before named groups. It is a
398// wrapper around DefaultCommander.Register.
399func Register(cmd Command, group string) {
400	DefaultCommander.Register(cmd, group)
401}
402
403// ImportantFlag marks a top-level flag as important, which means it
404// will be printed out as part of the output of an ordinary "help"
405// subcommand.  (All flags, important or not, are printed by the
406// "flags" subcommand.) It is a wrapper around
407// DefaultCommander.ImportantFlag.
408func ImportantFlag(name string) {
409	DefaultCommander.ImportantFlag(name)
410}
411
412// Execute should be called once the default flags have been
413// initialized by flag.Parse. It finds the correct subcommand and
414// executes it, and returns an ExitStatus with the result. On a usage
415// error, an appropriate message is printed to os.Stderr, and
416// ExitUsageError is returned. The additional args are provided as-is
417// to the Execute method of the selected Command. It is a wrapper
418// around DefaultCommander.Execute.
419func Execute(ctx context.Context, args ...interface{}) ExitStatus {
420	return DefaultCommander.Execute(ctx, args...)
421}
422
423// HelpCommand returns a Command which implements "help" for the
424// DefaultCommander. Use Register(HelpCommand(), <group>) for it to be
425// recognized.
426func HelpCommand() Command {
427	return DefaultCommander.HelpCommand()
428}
429
430// FlagsCommand returns a Command which implements "flags" for the
431// DefaultCommander. Use Register(FlagsCommand(), <group>) for it to be
432// recognized.
433func FlagsCommand() Command {
434	return DefaultCommander.FlagsCommand()
435}
436
437// CommandsCommand returns Command which implements a "commands" subcommand.
438func CommandsCommand() Command {
439	return DefaultCommander.CommandsCommand()
440}
441