1package cli
2
3import (
4	"fmt"
5	"io"
6	"io/ioutil"
7	"os"
8	"time"
9)
10
11// App is the main structure of a cli application. It is recomended that
12// an app be created with the cli.NewApp() function
13type App struct {
14	// The name of the program. Defaults to os.Args[0]
15	Name string
16	// Description of the program.
17	Usage string
18	// Version of the program
19	Version string
20	// List of commands to execute
21	Commands []Command
22	// List of help topics (help not associated with a command)
23	HelpTopics []HelpTopic
24	// List of flags to parse
25	Flags []Flag
26	// Boolean to enable bash completion commands
27	EnableBashCompletion bool
28	// Boolean to hide built-in help command
29	HideHelp bool
30	// Boolean to hide built-in version flag
31	HideVersion bool
32	// An action to execute when the bash-completion flag is set
33	BashComplete func(context *Context)
34	// An action to execute before any subcommands are run, but after the context is ready
35	// If a non-nil error is returned, no subcommands are run
36	Before func(context *Context) error
37	// An action to execute after any subcommands are run, but after the subcommand has finished
38	// It is run even if Action() panics
39	After func(context *Context) error
40	// The action to execute when no subcommands are specified
41	Action func(context *Context)
42	// Execute this function if the proper command cannot be found
43	CommandNotFound func(context *Context, command string)
44	// Compilation date
45	Compiled time.Time
46	// List of all authors who contributed
47	Authors []Author
48	// Copyright of the binary if any
49	Copyright string
50	// Name of Author (Note: Use App.Authors, this is deprecated)
51	Author string
52	// Email of Author (Note: Use App.Authors, this is deprecated)
53	Email string
54	// Writer writer to write output to
55	Writer io.Writer
56}
57
58// Tries to find out when this binary was compiled.
59// Returns the current time if it fails to find it.
60func compileTime() time.Time {
61	info, err := os.Stat(os.Args[0])
62	if err != nil {
63		return time.Now()
64	}
65	return info.ModTime()
66}
67
68// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
69func NewApp() *App {
70	return &App{
71		Name:         os.Args[0],
72		Usage:        "A new cli application",
73		Version:      "0.0.0",
74		BashComplete: DefaultAppComplete,
75		Action:       helpCommand.Action,
76		Compiled:     time.Now(), // avoid compileTime() because of the Stat()
77		Writer:       os.Stdout,
78	}
79}
80
81// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
82func (a *App) Run(arguments []string) (err error) {
83	if a.Author != "" || a.Email != "" {
84		a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
85	}
86
87	// append help to commands
88	if a.Command(helpCommand.Name) == nil && !a.HideHelp {
89		a.Commands = append(a.Commands, helpCommand)
90		if (HelpFlag != BoolFlag{}) {
91			a.appendFlag(HelpFlag)
92		}
93	}
94
95	//append version/help flags
96	if a.EnableBashCompletion {
97		a.appendFlag(BashCompletionFlag)
98	}
99
100	if !a.HideVersion {
101		a.appendFlag(VersionFlag)
102	}
103
104	// parse flags
105	set := flagSet(a.Name, a.Flags)
106	set.SetOutput(ioutil.Discard)
107	err = set.Parse(arguments[1:])
108	nerr := normalizeFlags(a.Flags, set)
109	if nerr != nil {
110		fmt.Fprintln(a.Writer, nerr)
111		context := NewContext(a, set, nil)
112		ShowAppHelp(context)
113		return nerr
114	}
115	context := NewContext(a, set, nil)
116
117	if err != nil {
118		fmt.Fprintln(a.Writer, "Incorrect Usage.")
119		fmt.Fprintln(a.Writer)
120		ShowAppHelp(context)
121		return err
122	}
123
124	if checkCompletions(context) {
125		return nil
126	}
127
128	if checkHelp(context) {
129		return nil
130	}
131
132	if checkVersion(context) {
133		return nil
134	}
135
136	if a.After != nil {
137		defer func() {
138			afterErr := a.After(context)
139			if afterErr != nil {
140				if err != nil {
141					err = NewMultiError(err, afterErr)
142				} else {
143					err = afterErr
144				}
145			}
146		}()
147	}
148
149	if a.Before != nil {
150		err := a.Before(context)
151		if err != nil {
152			return err
153		}
154	}
155
156	args := context.Args()
157	if args.Present() {
158		name := args.First()
159		c := a.Command(name)
160		if c != nil {
161			return c.Run(context)
162		}
163	}
164
165	// Run default Action
166	a.Action(context)
167	return nil
168}
169
170// Another entry point to the cli app, takes care of passing arguments and error handling
171func (a *App) RunAndExitOnError() {
172	if err := a.Run(os.Args); err != nil {
173		fmt.Fprintln(os.Stderr, err)
174		os.Exit(1)
175	}
176}
177
178// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
179func (a *App) RunAsSubcommand(ctx *Context) (err error) {
180	// append help to commands
181	if len(a.Commands) > 0 {
182		if a.Command(helpCommand.Name) == nil && !a.HideHelp {
183			a.Commands = append(a.Commands, helpCommand)
184			if (HelpFlag != BoolFlag{}) {
185				a.appendFlag(HelpFlag)
186			}
187		}
188	}
189
190	// append flags
191	if a.EnableBashCompletion {
192		a.appendFlag(BashCompletionFlag)
193	}
194
195	// parse flags
196	set := flagSet(a.Name, a.Flags)
197	set.SetOutput(ioutil.Discard)
198	err = set.Parse(ctx.Args().Tail())
199	nerr := normalizeFlags(a.Flags, set)
200	context := NewContext(a, set, ctx)
201
202	if nerr != nil {
203		fmt.Fprintln(a.Writer, nerr)
204		fmt.Fprintln(a.Writer)
205		if len(a.Commands) > 0 {
206			ShowSubcommandHelp(context)
207		} else {
208			ShowCommandHelp(ctx, context.Args().First())
209		}
210		return nerr
211	}
212
213	if err != nil {
214		fmt.Fprintln(a.Writer, "Incorrect Usage.")
215		fmt.Fprintln(a.Writer)
216		ShowSubcommandHelp(context)
217		return err
218	}
219
220	if checkCompletions(context) {
221		return nil
222	}
223
224	if len(a.Commands) > 0 {
225		if checkSubcommandHelp(context) {
226			return nil
227		}
228	} else {
229		if checkCommandHelp(ctx, context.Args().First()) {
230			return nil
231		}
232	}
233
234	if a.After != nil {
235		defer func() {
236			afterErr := a.After(context)
237			if afterErr != nil {
238				if err != nil {
239					err = NewMultiError(err, afterErr)
240				} else {
241					err = afterErr
242				}
243			}
244		}()
245	}
246
247	if a.Before != nil {
248		err := a.Before(context)
249		if err != nil {
250			return err
251		}
252	}
253
254	args := context.Args()
255	if args.Present() {
256		name := args.First()
257		c := a.Command(name)
258		if c != nil {
259			return c.Run(context)
260		}
261	}
262
263	// Run default Action
264	a.Action(context)
265
266	return nil
267}
268
269// Returns the named command on App. Returns nil if the command does not exist
270func (a *App) Command(name string) *Command {
271	for _, c := range a.Commands {
272		if c.HasName(name) {
273			return &c
274		}
275	}
276
277	return nil
278}
279
280func (a *App) hasFlag(flag Flag) bool {
281	for _, f := range a.Flags {
282		if flag == f {
283			return true
284		}
285	}
286
287	return false
288}
289
290func (a *App) appendFlag(flag Flag) {
291	if !a.hasFlag(flag) {
292		a.Flags = append(a.Flags, flag)
293	}
294}
295
296// Author represents someone who has contributed to a cli project.
297type Author struct {
298	Name  string // The Authors name
299	Email string // The Authors email
300}
301
302// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
303func (a Author) String() string {
304	e := ""
305	if a.Email != "" {
306		e = "<" + a.Email + "> "
307	}
308
309	return fmt.Sprintf("%v %v", a.Name, e)
310}
311