1// Licensed under terms of MIT license (see LICENSE-MIT)
2// Copyright (c) 2013 Keith Batten, kbatten@gmail.com
3// Copyright (c) 2016 David Irvine
4
5package docopt
6
7import (
8	"fmt"
9	"os"
10	"regexp"
11	"strings"
12)
13
14type Parser struct {
15	// HelpHandler is called when we encounter bad user input, or when the user
16	// asks for help.
17	// By default, this calls os.Exit(0) if it handled a built-in option such
18	// as -h, --help or --version. If the user errored with a wrong command or
19	// options, we exit with a return code of 1.
20	HelpHandler func(err error, usage string)
21	// OptionsFirst requires that option flags always come before positional
22	// arguments; otherwise they can overlap.
23	OptionsFirst bool
24	// SkipHelpFlags tells the parser not to look for -h and --help flags and
25	// call the HelpHandler.
26	SkipHelpFlags bool
27}
28
29var PrintHelpAndExit = func(err error, usage string) {
30	if err != nil {
31		fmt.Fprintln(os.Stderr, usage)
32		os.Exit(1)
33	} else {
34		fmt.Println(usage)
35		os.Exit(0)
36	}
37}
38
39var PrintHelpOnly = func(err error, usage string) {
40	if err != nil {
41		fmt.Fprintln(os.Stderr, usage)
42	} else {
43		fmt.Println(usage)
44	}
45}
46
47var NoHelpHandler = func(err error, usage string) {}
48
49var DefaultParser = &Parser{
50	HelpHandler:   PrintHelpAndExit,
51	OptionsFirst:  false,
52	SkipHelpFlags: false,
53}
54
55// ParseDoc parses os.Args[1:] based on the interface described in doc, using the default parser options.
56func ParseDoc(doc string) (Opts, error) {
57	return ParseArgs(doc, nil, "")
58}
59
60// ParseArgs parses custom arguments based on the interface described in doc. If you provide a non-empty version
61// string, then this will be displayed when the --version flag is found. This method uses the default parser options.
62func ParseArgs(doc string, argv []string, version string) (Opts, error) {
63	return DefaultParser.ParseArgs(doc, argv, version)
64}
65
66// ParseArgs parses custom arguments based on the interface described in doc. If you provide a non-empty version
67// string, then this will be displayed when the --version flag is found.
68func (p *Parser) ParseArgs(doc string, argv []string, version string) (Opts, error) {
69	return p.parse(doc, argv, version)
70}
71
72// Deprecated: Parse is provided for backward compatibility with the original docopt.go package.
73// Please rather make use of ParseDoc, ParseArgs, or use your own custom Parser.
74func Parse(doc string, argv []string, help bool, version string, optionsFirst bool, exit ...bool) (map[string]interface{}, error) {
75	exitOk := true
76	if len(exit) > 0 {
77		exitOk = exit[0]
78	}
79	p := &Parser{
80		OptionsFirst:  optionsFirst,
81		SkipHelpFlags: !help,
82	}
83	if exitOk {
84		p.HelpHandler = PrintHelpAndExit
85	} else {
86		p.HelpHandler = PrintHelpOnly
87	}
88	return p.parse(doc, argv, version)
89}
90
91func (p *Parser) parse(doc string, argv []string, version string) (map[string]interface{}, error) {
92	if argv == nil {
93		argv = os.Args[1:]
94	}
95	if p.HelpHandler == nil {
96		p.HelpHandler = DefaultParser.HelpHandler
97	}
98	args, output, err := parse(doc, argv, !p.SkipHelpFlags, version, p.OptionsFirst)
99	if _, ok := err.(*UserError); ok {
100		// the user gave us bad input
101		p.HelpHandler(err, output)
102	} else if len(output) > 0 && err == nil {
103		// the user asked for help or --version
104		p.HelpHandler(err, output)
105	}
106	return args, err
107}
108
109// -----------------------------------------------------------------------------
110
111// parse and return a map of args, output and all errors
112func parse(doc string, argv []string, help bool, version string, optionsFirst bool) (args map[string]interface{}, output string, err error) {
113	if argv == nil && len(os.Args) > 1 {
114		argv = os.Args[1:]
115	}
116
117	usageSections := parseSection("usage:", doc)
118
119	if len(usageSections) == 0 {
120		err = newLanguageError("\"usage:\" (case-insensitive) not found.")
121		return
122	}
123	if len(usageSections) > 1 {
124		err = newLanguageError("More than one \"usage:\" (case-insensitive).")
125		return
126	}
127	usage := usageSections[0]
128
129	options := parseDefaults(doc)
130	formal, err := formalUsage(usage)
131	if err != nil {
132		output = handleError(err, usage)
133		return
134	}
135
136	pat, err := parsePattern(formal, &options)
137	if err != nil {
138		output = handleError(err, usage)
139		return
140	}
141
142	patternArgv, err := parseArgv(newTokenList(argv, errorUser), &options, optionsFirst)
143	if err != nil {
144		output = handleError(err, usage)
145		return
146	}
147	patFlat, err := pat.flat(patternOption)
148	if err != nil {
149		output = handleError(err, usage)
150		return
151	}
152	patternOptions := patFlat.unique()
153
154	patFlat, err = pat.flat(patternOptionSSHORTCUT)
155	if err != nil {
156		output = handleError(err, usage)
157		return
158	}
159	for _, optionsShortcut := range patFlat {
160		docOptions := parseDefaults(doc)
161		optionsShortcut.children = docOptions.unique().diff(patternOptions)
162	}
163
164	if output = extras(help, version, patternArgv, doc); len(output) > 0 {
165		return
166	}
167
168	err = pat.fix()
169	if err != nil {
170		output = handleError(err, usage)
171		return
172	}
173	matched, left, collected := pat.match(&patternArgv, nil)
174	if matched && len(*left) == 0 {
175		patFlat, err = pat.flat(patternDefault)
176		if err != nil {
177			output = handleError(err, usage)
178			return
179		}
180		args = append(patFlat, *collected...).dictionary()
181		return
182	}
183
184	err = newUserError("")
185	output = handleError(err, usage)
186	return
187}
188
189func handleError(err error, usage string) string {
190	if _, ok := err.(*UserError); ok {
191		return strings.TrimSpace(fmt.Sprintf("%s\n%s", err, usage))
192	}
193	return ""
194}
195
196func parseSection(name, source string) []string {
197	p := regexp.MustCompile(`(?im)^([^\n]*` + name + `[^\n]*\n?(?:[ \t].*?(?:\n|$))*)`)
198	s := p.FindAllString(source, -1)
199	if s == nil {
200		s = []string{}
201	}
202	for i, v := range s {
203		s[i] = strings.TrimSpace(v)
204	}
205	return s
206}
207
208func parseDefaults(doc string) patternList {
209	defaults := patternList{}
210	p := regexp.MustCompile(`\n[ \t]*(-\S+?)`)
211	for _, s := range parseSection("options:", doc) {
212		// FIXME corner case "bla: options: --foo"
213		_, _, s = stringPartition(s, ":") // get rid of "options:"
214		split := p.Split("\n"+s, -1)[1:]
215		match := p.FindAllStringSubmatch("\n"+s, -1)
216		for i := range split {
217			optionDescription := match[i][1] + split[i]
218			if strings.HasPrefix(optionDescription, "-") {
219				defaults = append(defaults, parseOption(optionDescription))
220			}
221		}
222	}
223	return defaults
224}
225
226func parsePattern(source string, options *patternList) (*pattern, error) {
227	tokens := tokenListFromPattern(source)
228	result, err := parseExpr(tokens, options)
229	if err != nil {
230		return nil, err
231	}
232	if tokens.current() != nil {
233		return nil, tokens.errorFunc("unexpected ending: %s" + strings.Join(tokens.tokens, " "))
234	}
235	return newRequired(result...), nil
236}
237
238func parseArgv(tokens *tokenList, options *patternList, optionsFirst bool) (patternList, error) {
239	/*
240		Parse command-line argument vector.
241
242		If options_first:
243			argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
244		else:
245			argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
246	*/
247	parsed := patternList{}
248	for tokens.current() != nil {
249		if tokens.current().eq("--") {
250			for _, v := range tokens.tokens {
251				parsed = append(parsed, newArgument("", v))
252			}
253			return parsed, nil
254		} else if tokens.current().hasPrefix("--") {
255			pl, err := parseLong(tokens, options)
256			if err != nil {
257				return nil, err
258			}
259			parsed = append(parsed, pl...)
260		} else if tokens.current().hasPrefix("-") && !tokens.current().eq("-") {
261			ps, err := parseShorts(tokens, options)
262			if err != nil {
263				return nil, err
264			}
265			parsed = append(parsed, ps...)
266		} else if optionsFirst {
267			for _, v := range tokens.tokens {
268				parsed = append(parsed, newArgument("", v))
269			}
270			return parsed, nil
271		} else {
272			parsed = append(parsed, newArgument("", tokens.move().String()))
273		}
274	}
275	return parsed, nil
276}
277
278func parseOption(optionDescription string) *pattern {
279	optionDescription = strings.TrimSpace(optionDescription)
280	options, _, description := stringPartition(optionDescription, "  ")
281	options = strings.Replace(options, ",", " ", -1)
282	options = strings.Replace(options, "=", " ", -1)
283
284	short := ""
285	long := ""
286	argcount := 0
287	var value interface{}
288	value = false
289
290	reDefault := regexp.MustCompile(`(?i)\[default: (.*)\]`)
291	for _, s := range strings.Fields(options) {
292		if strings.HasPrefix(s, "--") {
293			long = s
294		} else if strings.HasPrefix(s, "-") {
295			short = s
296		} else {
297			argcount = 1
298		}
299		if argcount > 0 {
300			matched := reDefault.FindAllStringSubmatch(description, -1)
301			if len(matched) > 0 {
302				value = matched[0][1]
303			} else {
304				value = nil
305			}
306		}
307	}
308	return newOption(short, long, argcount, value)
309}
310
311func parseExpr(tokens *tokenList, options *patternList) (patternList, error) {
312	// expr ::= seq ( '|' seq )* ;
313	seq, err := parseSeq(tokens, options)
314	if err != nil {
315		return nil, err
316	}
317	if !tokens.current().eq("|") {
318		return seq, nil
319	}
320	var result patternList
321	if len(seq) > 1 {
322		result = patternList{newRequired(seq...)}
323	} else {
324		result = seq
325	}
326	for tokens.current().eq("|") {
327		tokens.move()
328		seq, err = parseSeq(tokens, options)
329		if err != nil {
330			return nil, err
331		}
332		if len(seq) > 1 {
333			result = append(result, newRequired(seq...))
334		} else {
335			result = append(result, seq...)
336		}
337	}
338	if len(result) > 1 {
339		return patternList{newEither(result...)}, nil
340	}
341	return result, nil
342}
343
344func parseSeq(tokens *tokenList, options *patternList) (patternList, error) {
345	// seq ::= ( atom [ '...' ] )* ;
346	result := patternList{}
347	for !tokens.current().match(true, "]", ")", "|") {
348		atom, err := parseAtom(tokens, options)
349		if err != nil {
350			return nil, err
351		}
352		if tokens.current().eq("...") {
353			atom = patternList{newOneOrMore(atom...)}
354			tokens.move()
355		}
356		result = append(result, atom...)
357	}
358	return result, nil
359}
360
361func parseAtom(tokens *tokenList, options *patternList) (patternList, error) {
362	// atom ::= '(' expr ')' | '[' expr ']' | 'options' | long | shorts | argument | command ;
363	tok := tokens.current()
364	result := patternList{}
365	if tokens.current().match(false, "(", "[") {
366		tokens.move()
367		var matching string
368		pl, err := parseExpr(tokens, options)
369		if err != nil {
370			return nil, err
371		}
372		if tok.eq("(") {
373			matching = ")"
374			result = patternList{newRequired(pl...)}
375		} else if tok.eq("[") {
376			matching = "]"
377			result = patternList{newOptional(pl...)}
378		}
379		moved := tokens.move()
380		if !moved.eq(matching) {
381			return nil, tokens.errorFunc("unmatched '%s', expected: '%s' got: '%s'", tok, matching, moved)
382		}
383		return result, nil
384	} else if tok.eq("options") {
385		tokens.move()
386		return patternList{newOptionsShortcut()}, nil
387	} else if tok.hasPrefix("--") && !tok.eq("--") {
388		return parseLong(tokens, options)
389	} else if tok.hasPrefix("-") && !tok.eq("-") && !tok.eq("--") {
390		return parseShorts(tokens, options)
391	} else if tok.hasPrefix("<") && tok.hasSuffix(">") || tok.isUpper() {
392		return patternList{newArgument(tokens.move().String(), nil)}, nil
393	}
394	return patternList{newCommand(tokens.move().String(), false)}, nil
395}
396
397func parseLong(tokens *tokenList, options *patternList) (patternList, error) {
398	// long ::= '--' chars [ ( ' ' | '=' ) chars ] ;
399	long, eq, v := stringPartition(tokens.move().String(), "=")
400	var value interface{}
401	var opt *pattern
402	if eq == "" && v == "" {
403		value = nil
404	} else {
405		value = v
406	}
407
408	if !strings.HasPrefix(long, "--") {
409		return nil, newError("long option '%s' doesn't start with --", long)
410	}
411	similar := patternList{}
412	for _, o := range *options {
413		if o.long == long {
414			similar = append(similar, o)
415		}
416	}
417	if tokens.err == errorUser && len(similar) == 0 { // if no exact match
418		similar = patternList{}
419		for _, o := range *options {
420			if strings.HasPrefix(o.long, long) {
421				similar = append(similar, o)
422			}
423		}
424	}
425	if len(similar) > 1 { // might be simply specified ambiguously 2+ times?
426		similarLong := make([]string, len(similar))
427		for i, s := range similar {
428			similarLong[i] = s.long
429		}
430		return nil, tokens.errorFunc("%s is not a unique prefix: %s?", long, strings.Join(similarLong, ", "))
431	} else if len(similar) < 1 {
432		argcount := 0
433		if eq == "=" {
434			argcount = 1
435		}
436		opt = newOption("", long, argcount, false)
437		*options = append(*options, opt)
438		if tokens.err == errorUser {
439			var val interface{}
440			if argcount > 0 {
441				val = value
442			} else {
443				val = true
444			}
445			opt = newOption("", long, argcount, val)
446		}
447	} else {
448		opt = newOption(similar[0].short, similar[0].long, similar[0].argcount, similar[0].value)
449		if opt.argcount == 0 {
450			if value != nil {
451				return nil, tokens.errorFunc("%s must not have an argument", opt.long)
452			}
453		} else {
454			if value == nil {
455				if tokens.current().match(true, "--") {
456					return nil, tokens.errorFunc("%s requires argument", opt.long)
457				}
458				moved := tokens.move()
459				if moved != nil {
460					value = moved.String() // only set as string if not nil
461				}
462			}
463		}
464		if tokens.err == errorUser {
465			if value != nil {
466				opt.value = value
467			} else {
468				opt.value = true
469			}
470		}
471	}
472
473	return patternList{opt}, nil
474}
475
476func parseShorts(tokens *tokenList, options *patternList) (patternList, error) {
477	// shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;
478	tok := tokens.move()
479	if !tok.hasPrefix("-") || tok.hasPrefix("--") {
480		return nil, newError("short option '%s' doesn't start with -", tok)
481	}
482	left := strings.TrimLeft(tok.String(), "-")
483	parsed := patternList{}
484	for left != "" {
485		var opt *pattern
486		short := "-" + left[0:1]
487		left = left[1:]
488		similar := patternList{}
489		for _, o := range *options {
490			if o.short == short {
491				similar = append(similar, o)
492			}
493		}
494		if len(similar) > 1 {
495			return nil, tokens.errorFunc("%s is specified ambiguously %d times", short, len(similar))
496		} else if len(similar) < 1 {
497			opt = newOption(short, "", 0, false)
498			*options = append(*options, opt)
499			if tokens.err == errorUser {
500				opt = newOption(short, "", 0, true)
501			}
502		} else { // why copying is necessary here?
503			opt = newOption(short, similar[0].long, similar[0].argcount, similar[0].value)
504			var value interface{}
505			if opt.argcount > 0 {
506				if left == "" {
507					if tokens.current().match(true, "--") {
508						return nil, tokens.errorFunc("%s requires argument", short)
509					}
510					value = tokens.move().String()
511				} else {
512					value = left
513					left = ""
514				}
515			}
516			if tokens.err == errorUser {
517				if value != nil {
518					opt.value = value
519				} else {
520					opt.value = true
521				}
522			}
523		}
524		parsed = append(parsed, opt)
525	}
526	return parsed, nil
527}
528
529func formalUsage(section string) (string, error) {
530	_, _, section = stringPartition(section, ":") // drop "usage:"
531	pu := strings.Fields(section)
532
533	if len(pu) == 0 {
534		return "", newLanguageError("no fields found in usage (perhaps a spacing error).")
535	}
536
537	result := "( "
538	for _, s := range pu[1:] {
539		if s == pu[0] {
540			result += ") | ( "
541		} else {
542			result += s + " "
543		}
544	}
545	result += ")"
546
547	return result, nil
548}
549
550func extras(help bool, version string, options patternList, doc string) string {
551	if help {
552		for _, o := range options {
553			if (o.name == "-h" || o.name == "--help") && o.value == true {
554				return strings.Trim(doc, "\n")
555			}
556		}
557	}
558	if version != "" {
559		for _, o := range options {
560			if (o.name == "--version") && o.value == true {
561				return version
562			}
563		}
564	}
565	return ""
566}
567
568func stringPartition(s, sep string) (string, string, string) {
569	sepPos := strings.Index(s, sep)
570	if sepPos == -1 { // no seperator found
571		return s, "", ""
572	}
573	split := strings.SplitN(s, sep, 2)
574	return split[0], sep, split[1]
575}
576