1package kingpin
2
3import (
4	"fmt"
5	"io"
6	"os"
7	"regexp"
8	"strings"
9)
10
11var (
12	ErrCommandNotSpecified = fmt.Errorf("command not specified")
13)
14
15var (
16	envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
17)
18
19type ApplicationValidator func(*Application) error
20
21// An Application contains the definitions of flags, arguments and commands
22// for an application.
23type Application struct {
24	cmdMixin
25	initialized bool
26
27	Name string
28	Help string
29
30	author         string
31	version        string
32	errorWriter    io.Writer // Destination for errors.
33	usageWriter    io.Writer // Destination for usage
34	usageTemplate  string
35	validator      ApplicationValidator
36	terminate      func(status int) // See Terminate()
37	noInterspersed bool             // can flags be interspersed with args (or must they come first)
38	defaultEnvars  bool
39	completion     bool
40
41	// Help flag. Exposed for user customisation.
42	HelpFlag *FlagClause
43	// Help command. Exposed for user customisation. May be nil.
44	HelpCommand *CmdClause
45	// Version flag. Exposed for user customisation. May be nil.
46	VersionFlag *FlagClause
47}
48
49// New creates a new Kingpin application instance.
50func New(name, help string) *Application {
51	a := &Application{
52		Name:          name,
53		Help:          help,
54		errorWriter:   os.Stderr, // Left for backwards compatibility purposes.
55		usageWriter:   os.Stderr,
56		usageTemplate: DefaultUsageTemplate,
57		terminate:     os.Exit,
58	}
59	a.flagGroup = newFlagGroup()
60	a.argGroup = newArgGroup()
61	a.cmdGroup = newCmdGroup(a)
62	a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).")
63	a.HelpFlag.Bool()
64	a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool()
65	a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool()
66	a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion)
67	a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool()
68	a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool()
69
70	return a
71}
72
73func (a *Application) generateLongHelp(c *ParseContext) error {
74	a.Writer(os.Stdout)
75	if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil {
76		return err
77	}
78	a.terminate(0)
79	return nil
80}
81
82func (a *Application) generateManPage(c *ParseContext) error {
83	a.Writer(os.Stdout)
84	if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil {
85		return err
86	}
87	a.terminate(0)
88	return nil
89}
90
91func (a *Application) generateBashCompletionScript(c *ParseContext) error {
92	a.Writer(os.Stdout)
93	if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil {
94		return err
95	}
96	a.terminate(0)
97	return nil
98}
99
100func (a *Application) generateZSHCompletionScript(c *ParseContext) error {
101	a.Writer(os.Stdout)
102	if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil {
103		return err
104	}
105	a.terminate(0)
106	return nil
107}
108
109// DefaultEnvars configures all flags (that do not already have an associated
110// envar) to use a default environment variable in the form "<app>_<flag>".
111//
112// For example, if the application is named "foo" and a flag is named "bar-
113// waz" the environment variable: "FOO_BAR_WAZ".
114func (a *Application) DefaultEnvars() *Application {
115	a.defaultEnvars = true
116	return a
117}
118
119// Terminate specifies the termination handler. Defaults to os.Exit(status).
120// If nil is passed, a no-op function will be used.
121func (a *Application) Terminate(terminate func(int)) *Application {
122	if terminate == nil {
123		terminate = func(int) {}
124	}
125	a.terminate = terminate
126	return a
127}
128
129// Writer specifies the writer to use for usage and errors. Defaults to os.Stderr.
130// DEPRECATED: See ErrorWriter and UsageWriter.
131func (a *Application) Writer(w io.Writer) *Application {
132	a.errorWriter = w
133	a.usageWriter = w
134	return a
135}
136
137// ErrorWriter sets the io.Writer to use for errors.
138func (a *Application) ErrorWriter(w io.Writer) *Application {
139	a.errorWriter = w
140	return a
141}
142
143// UsageWriter sets the io.Writer to use for errors.
144func (a *Application) UsageWriter(w io.Writer) *Application {
145	a.usageWriter = w
146	return a
147}
148
149// UsageTemplate specifies the text template to use when displaying usage
150// information. The default is UsageTemplate.
151func (a *Application) UsageTemplate(template string) *Application {
152	a.usageTemplate = template
153	return a
154}
155
156// Validate sets a validation function to run when parsing.
157func (a *Application) Validate(validator ApplicationValidator) *Application {
158	a.validator = validator
159	return a
160}
161
162// ParseContext parses the given command line and returns the fully populated
163// ParseContext.
164func (a *Application) ParseContext(args []string) (*ParseContext, error) {
165	return a.parseContext(false, args)
166}
167
168func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) {
169	if err := a.init(); err != nil {
170		return nil, err
171	}
172	context := tokenize(args, ignoreDefault)
173	err := parse(context, a)
174	return context, err
175}
176
177// Parse parses command-line arguments. It returns the selected command and an
178// error. The selected command will be a space separated subcommand, if
179// subcommands have been configured.
180//
181// This will populate all flag and argument values, call all callbacks, and so
182// on.
183func (a *Application) Parse(args []string) (command string, err error) {
184
185	context, parseErr := a.ParseContext(args)
186	selected := []string{}
187	var setValuesErr error
188
189	if context == nil {
190		// Since we do not throw error immediately, there could be a case
191		// where a context returns nil. Protect against that.
192		return "", parseErr
193	}
194
195	if err = a.setDefaults(context); err != nil {
196		return "", err
197	}
198
199	selected, setValuesErr = a.setValues(context)
200
201	if err = a.applyPreActions(context, !a.completion); err != nil {
202		return "", err
203	}
204
205	if a.completion {
206		a.generateBashCompletion(context)
207		a.terminate(0)
208	} else {
209		if parseErr != nil {
210			return "", parseErr
211		}
212
213		a.maybeHelp(context)
214		if !context.EOL() {
215			return "", fmt.Errorf("unexpected argument '%s'", context.Peek())
216		}
217
218		if setValuesErr != nil {
219			return "", setValuesErr
220		}
221
222		command, err = a.execute(context, selected)
223		if err == ErrCommandNotSpecified {
224			a.writeUsage(context, nil)
225		}
226	}
227	return command, err
228}
229
230func (a *Application) writeUsage(context *ParseContext, err error) {
231	if err != nil {
232		a.Errorf("%s", err)
233	}
234	if err := a.UsageForContext(context); err != nil {
235		panic(err)
236	}
237	if err != nil {
238		a.terminate(1)
239	} else {
240		a.terminate(0)
241	}
242}
243
244func (a *Application) maybeHelp(context *ParseContext) {
245	for _, element := range context.Elements {
246		if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag {
247			// Re-parse the command-line ignoring defaults, so that help works correctly.
248			context, _ = a.parseContext(true, context.rawArgs)
249			a.writeUsage(context, nil)
250		}
251	}
252}
253
254// Version adds a --version flag for displaying the application version.
255func (a *Application) Version(version string) *Application {
256	a.version = version
257	a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error {
258		fmt.Fprintln(a.usageWriter, version)
259		a.terminate(0)
260		return nil
261	})
262	a.VersionFlag.Bool()
263	return a
264}
265
266// Author sets the author output by some help templates.
267func (a *Application) Author(author string) *Application {
268	a.author = author
269	return a
270}
271
272// Action callback to call when all values are populated and parsing is
273// complete, but before any command, flag or argument actions.
274//
275// All Action() callbacks are called in the order they are encountered on the
276// command line.
277func (a *Application) Action(action Action) *Application {
278	a.addAction(action)
279	return a
280}
281
282// Action called after parsing completes but before validation and execution.
283func (a *Application) PreAction(action Action) *Application {
284	a.addPreAction(action)
285	return a
286}
287
288// Command adds a new top-level command.
289func (a *Application) Command(name, help string) *CmdClause {
290	return a.addCommand(name, help)
291}
292
293// Interspersed control if flags can be interspersed with positional arguments
294//
295// true (the default) means that they can, false means that all the flags must appear before the first positional arguments.
296func (a *Application) Interspersed(interspersed bool) *Application {
297	a.noInterspersed = !interspersed
298	return a
299}
300
301func (a *Application) defaultEnvarPrefix() string {
302	if a.defaultEnvars {
303		return a.Name
304	}
305	return ""
306}
307
308func (a *Application) init() error {
309	if a.initialized {
310		return nil
311	}
312	if a.cmdGroup.have() && a.argGroup.have() {
313		return fmt.Errorf("can't mix top-level Arg()s with Command()s")
314	}
315
316	// If we have subcommands, add a help command at the top-level.
317	if a.cmdGroup.have() {
318		var command []string
319		a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error {
320			a.Usage(command)
321			a.terminate(0)
322			return nil
323		})
324		a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command)
325		// Make help first command.
326		l := len(a.commandOrder)
327		a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...)
328	}
329
330	if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil {
331		return err
332	}
333	if err := a.cmdGroup.init(); err != nil {
334		return err
335	}
336	if err := a.argGroup.init(); err != nil {
337		return err
338	}
339	for _, cmd := range a.commands {
340		if err := cmd.init(); err != nil {
341			return err
342		}
343	}
344	flagGroups := []*flagGroup{a.flagGroup}
345	for _, cmd := range a.commandOrder {
346		if err := checkDuplicateFlags(cmd, flagGroups); err != nil {
347			return err
348		}
349	}
350	a.initialized = true
351	return nil
352}
353
354// Recursively check commands for duplicate flags.
355func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error {
356	// Check for duplicates.
357	for _, flags := range flagGroups {
358		for _, flag := range current.flagOrder {
359			if flag.shorthand != 0 {
360				if _, ok := flags.short[string(flag.shorthand)]; ok {
361					return fmt.Errorf("duplicate short flag -%c", flag.shorthand)
362				}
363			}
364			if _, ok := flags.long[flag.name]; ok {
365				return fmt.Errorf("duplicate long flag --%s", flag.name)
366			}
367		}
368	}
369	flagGroups = append(flagGroups, current.flagGroup)
370	// Check subcommands.
371	for _, subcmd := range current.commandOrder {
372		if err := checkDuplicateFlags(subcmd, flagGroups); err != nil {
373			return err
374		}
375	}
376	return nil
377}
378
379func (a *Application) execute(context *ParseContext, selected []string) (string, error) {
380	var err error
381
382	if err = a.validateRequired(context); err != nil {
383		return "", err
384	}
385
386	if err = a.applyValidators(context); err != nil {
387		return "", err
388	}
389
390	if err = a.applyActions(context); err != nil {
391		return "", err
392	}
393
394	command := strings.Join(selected, " ")
395	if command == "" && a.cmdGroup.have() {
396		return "", ErrCommandNotSpecified
397	}
398	return command, err
399}
400
401func (a *Application) setDefaults(context *ParseContext) error {
402	flagElements := map[string]*ParseElement{}
403	for _, element := range context.Elements {
404		if flag, ok := element.Clause.(*FlagClause); ok {
405			if flag.name == "help" {
406				return nil
407			}
408			flagElements[flag.name] = element
409		}
410	}
411
412	argElements := map[string]*ParseElement{}
413	for _, element := range context.Elements {
414		if arg, ok := element.Clause.(*ArgClause); ok {
415			argElements[arg.name] = element
416		}
417	}
418
419	// Check required flags and set defaults.
420	for _, flag := range context.flags.long {
421		if flagElements[flag.name] == nil {
422			if err := flag.setDefault(); err != nil {
423				return err
424			}
425		}
426	}
427
428	for _, arg := range context.arguments.args {
429		if argElements[arg.name] == nil {
430			if err := arg.setDefault(); err != nil {
431				return err
432			}
433		}
434	}
435
436	return nil
437}
438
439func (a *Application) validateRequired(context *ParseContext) error {
440	flagElements := map[string]*ParseElement{}
441	for _, element := range context.Elements {
442		if flag, ok := element.Clause.(*FlagClause); ok {
443			flagElements[flag.name] = element
444		}
445	}
446
447	argElements := map[string]*ParseElement{}
448	for _, element := range context.Elements {
449		if arg, ok := element.Clause.(*ArgClause); ok {
450			argElements[arg.name] = element
451		}
452	}
453
454	// Check required flags and set defaults.
455	for _, flag := range context.flags.long {
456		if flagElements[flag.name] == nil {
457			// Check required flags were provided.
458			if flag.needsValue() {
459				return fmt.Errorf("required flag --%s not provided", flag.name)
460			}
461		}
462	}
463
464	for _, arg := range context.arguments.args {
465		if argElements[arg.name] == nil {
466			if arg.needsValue() {
467				return fmt.Errorf("required argument '%s' not provided", arg.name)
468			}
469		}
470	}
471	return nil
472}
473
474func (a *Application) setValues(context *ParseContext) (selected []string, err error) {
475	// Set all arg and flag values.
476	var (
477		lastCmd *CmdClause
478		flagSet = map[string]struct{}{}
479	)
480	for _, element := range context.Elements {
481		switch clause := element.Clause.(type) {
482		case *FlagClause:
483			if _, ok := flagSet[clause.name]; ok {
484				if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() {
485					return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name)
486				}
487			}
488			if err = clause.value.Set(*element.Value); err != nil {
489				return
490			}
491			flagSet[clause.name] = struct{}{}
492
493		case *ArgClause:
494			if err = clause.value.Set(*element.Value); err != nil {
495				return
496			}
497
498		case *CmdClause:
499			if clause.validator != nil {
500				if err = clause.validator(clause); err != nil {
501					return
502				}
503			}
504			selected = append(selected, clause.name)
505			lastCmd = clause
506		}
507	}
508
509	if lastCmd != nil && len(lastCmd.commands) > 0 {
510		return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand())
511	}
512
513	return
514}
515
516func (a *Application) applyValidators(context *ParseContext) (err error) {
517	// Call command validation functions.
518	for _, element := range context.Elements {
519		if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil {
520			if err = cmd.validator(cmd); err != nil {
521				return err
522			}
523		}
524	}
525
526	if a.validator != nil {
527		err = a.validator(a)
528	}
529	return err
530}
531
532func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error {
533	if err := a.actionMixin.applyPreActions(context); err != nil {
534		return err
535	}
536	// Dispatch to actions.
537	if dispatch {
538		for _, element := range context.Elements {
539			if applier, ok := element.Clause.(actionApplier); ok {
540				if err := applier.applyPreActions(context); err != nil {
541					return err
542				}
543			}
544		}
545	}
546
547	return nil
548}
549
550func (a *Application) applyActions(context *ParseContext) error {
551	if err := a.actionMixin.applyActions(context); err != nil {
552		return err
553	}
554	// Dispatch to actions.
555	for _, element := range context.Elements {
556		if applier, ok := element.Clause.(actionApplier); ok {
557			if err := applier.applyActions(context); err != nil {
558				return err
559			}
560		}
561	}
562	return nil
563}
564
565// Errorf prints an error message to w in the format "<appname>: error: <message>".
566func (a *Application) Errorf(format string, args ...interface{}) {
567	fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...)
568}
569
570// Fatalf writes a formatted error to w then terminates with exit status 1.
571func (a *Application) Fatalf(format string, args ...interface{}) {
572	a.Errorf(format, args...)
573	a.terminate(1)
574}
575
576// FatalUsage prints an error message followed by usage information, then
577// exits with a non-zero status.
578func (a *Application) FatalUsage(format string, args ...interface{}) {
579	a.Errorf(format, args...)
580	// Force usage to go to error output.
581	a.usageWriter = a.errorWriter
582	a.Usage([]string{})
583	a.terminate(1)
584}
585
586// FatalUsageContext writes a printf formatted error message to w, then usage
587// information for the given ParseContext, before exiting.
588func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) {
589	a.Errorf(format, args...)
590	if err := a.UsageForContext(context); err != nil {
591		panic(err)
592	}
593	a.terminate(1)
594}
595
596// FatalIfError prints an error and exits if err is not nil. The error is printed
597// with the given formatted string, if any.
598func (a *Application) FatalIfError(err error, format string, args ...interface{}) {
599	if err != nil {
600		prefix := ""
601		if format != "" {
602			prefix = fmt.Sprintf(format, args...) + ": "
603		}
604		a.Errorf(prefix+"%s", err)
605		a.terminate(1)
606	}
607}
608
609func (a *Application) completionOptions(context *ParseContext) []string {
610	args := context.rawArgs
611
612	var (
613		currArg string
614		prevArg string
615		target  cmdMixin
616	)
617
618	numArgs := len(args)
619	if numArgs > 1 {
620		args = args[1:]
621		currArg = args[len(args)-1]
622	}
623	if numArgs > 2 {
624		prevArg = args[len(args)-2]
625	}
626
627	target = a.cmdMixin
628	if context.SelectedCommand != nil {
629		// A subcommand was in use. We will use it as the target
630		target = context.SelectedCommand.cmdMixin
631	}
632
633	if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") {
634		// Perform completion for A flag. The last/current argument started with "-"
635		var (
636			flagName  string // The name of a flag if given (could be half complete)
637			flagValue string // The value assigned to a flag (if given) (could be half complete)
638		)
639
640		if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") {
641			// Matches: 	./myApp --flag value
642			// Wont Match: 	./myApp --flag --
643			flagName = prevArg[2:] // Strip the "--"
644			flagValue = currArg
645		} else if strings.HasPrefix(currArg, "--") {
646			// Matches: 	./myApp --flag --
647			// Matches:		./myApp --flag somevalue --
648			// Matches: 	./myApp --
649			flagName = currArg[2:] // Strip the "--"
650		}
651
652		options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue)
653		if valueMatched {
654			// Value Matched. Show cmdCompletions
655			return target.CmdCompletion(context)
656		}
657
658		// Add top level flags if we're not at the top level and no match was found.
659		if context.SelectedCommand != nil && !flagMatched {
660			topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue)
661			if topValueMatched {
662				// Value Matched. Back to cmdCompletions
663				return target.CmdCompletion(context)
664			}
665
666			if topFlagMatched {
667				// Top level had a flag which matched the input. Return it's options.
668				options = topOptions
669			} else {
670				// Add top level flags
671				options = append(options, topOptions...)
672			}
673		}
674		return options
675	}
676
677	// Perform completion for sub commands and arguments.
678	return target.CmdCompletion(context)
679}
680
681func (a *Application) generateBashCompletion(context *ParseContext) {
682	options := a.completionOptions(context)
683	fmt.Printf("%s", strings.Join(options, "\n"))
684}
685
686func envarTransform(name string) string {
687	return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_"))
688}
689