1package kingpin
2
3import (
4	"bufio"
5	"fmt"
6	"os"
7	"strings"
8	"unicode/utf8"
9)
10
11type TokenType int
12
13// Token types.
14const (
15	TokenShort TokenType = iota
16	TokenLong
17	TokenArg
18	TokenError
19	TokenEOL
20)
21
22func (t TokenType) String() string {
23	switch t {
24	case TokenShort:
25		return "short flag"
26	case TokenLong:
27		return "long flag"
28	case TokenArg:
29		return "argument"
30	case TokenError:
31		return "error"
32	case TokenEOL:
33		return "<EOL>"
34	}
35	return "?"
36}
37
38var (
39	TokenEOLMarker = Token{-1, TokenEOL, ""}
40)
41
42type Token struct {
43	Index int
44	Type  TokenType
45	Value string
46}
47
48func (t *Token) Equal(o *Token) bool {
49	return t.Index == o.Index
50}
51
52func (t *Token) IsFlag() bool {
53	return t.Type == TokenShort || t.Type == TokenLong
54}
55
56func (t *Token) IsEOF() bool {
57	return t.Type == TokenEOL
58}
59
60func (t *Token) String() string {
61	switch t.Type {
62	case TokenShort:
63		return "-" + t.Value
64	case TokenLong:
65		return "--" + t.Value
66	case TokenArg:
67		return t.Value
68	case TokenError:
69		return "error: " + t.Value
70	case TokenEOL:
71		return "<EOL>"
72	default:
73		panic("unhandled type")
74	}
75}
76
77// A union of possible elements in a parse stack.
78type ParseElement struct {
79	// Clause is either *CmdClause, *ArgClause or *FlagClause.
80	Clause interface{}
81	// Value is corresponding value for an ArgClause or FlagClause (if any).
82	Value *string
83}
84
85// ParseContext holds the current context of the parser. When passed to
86// Action() callbacks Elements will be fully populated with *FlagClause,
87// *ArgClause and *CmdClause values and their corresponding arguments (if
88// any).
89type ParseContext struct {
90	SelectedCommand *CmdClause
91	ignoreDefault   bool
92	argsOnly        bool
93	peek            []*Token
94	argi            int // Index of current command-line arg we're processing.
95	args            []string
96	rawArgs         []string
97	flags           *flagGroup
98	arguments       *argGroup
99	argumenti       int // Cursor into arguments
100	// Flags, arguments and commands encountered and collected during parse.
101	Elements []*ParseElement
102}
103
104func (p *ParseContext) nextArg() *ArgClause {
105	if p.argumenti >= len(p.arguments.args) {
106		return nil
107	}
108	arg := p.arguments.args[p.argumenti]
109	if !arg.consumesRemainder() {
110		p.argumenti++
111	}
112	return arg
113}
114
115func (p *ParseContext) next() {
116	p.argi++
117	p.args = p.args[1:]
118}
119
120// HasTrailingArgs returns true if there are unparsed command-line arguments.
121// This can occur if the parser can not match remaining arguments.
122func (p *ParseContext) HasTrailingArgs() bool {
123	return len(p.args) > 0
124}
125
126func tokenize(args []string, ignoreDefault bool) *ParseContext {
127	return &ParseContext{
128		ignoreDefault: ignoreDefault,
129		args:          args,
130		rawArgs:       args,
131		flags:         newFlagGroup(),
132		arguments:     newArgGroup(),
133	}
134}
135
136func (p *ParseContext) mergeFlags(flags *flagGroup) {
137	for _, flag := range flags.flagOrder {
138		if flag.shorthand != 0 {
139			p.flags.short[string(flag.shorthand)] = flag
140		}
141		p.flags.long[flag.name] = flag
142		p.flags.flagOrder = append(p.flags.flagOrder, flag)
143	}
144}
145
146func (p *ParseContext) mergeArgs(args *argGroup) {
147	for _, arg := range args.args {
148		p.arguments.args = append(p.arguments.args, arg)
149	}
150}
151
152func (p *ParseContext) EOL() bool {
153	return p.Peek().Type == TokenEOL
154}
155
156func (p *ParseContext) Error() bool {
157	return p.Peek().Type == TokenError
158}
159
160// Next token in the parse context.
161func (p *ParseContext) Next() *Token {
162	if len(p.peek) > 0 {
163		return p.pop()
164	}
165
166	// End of tokens.
167	if len(p.args) == 0 {
168		return &Token{Index: p.argi, Type: TokenEOL}
169	}
170
171	arg := p.args[0]
172	p.next()
173
174	if p.argsOnly {
175		return &Token{p.argi, TokenArg, arg}
176	}
177
178	// All remaining args are passed directly.
179	if arg == "--" {
180		p.argsOnly = true
181		return p.Next()
182	}
183
184	if strings.HasPrefix(arg, "--") {
185		parts := strings.SplitN(arg[2:], "=", 2)
186		token := &Token{p.argi, TokenLong, parts[0]}
187		if len(parts) == 2 {
188			p.Push(&Token{p.argi, TokenArg, parts[1]})
189		}
190		return token
191	}
192
193	if strings.HasPrefix(arg, "-") {
194		if len(arg) == 1 {
195			return &Token{Index: p.argi, Type: TokenShort}
196		}
197		shortRune, size := utf8.DecodeRuneInString(arg[1:])
198		short := string(shortRune)
199		flag, ok := p.flags.short[short]
200		// Not a known short flag, we'll just return it anyway.
201		if !ok {
202		} else if fb, ok := flag.value.(boolFlag); ok && fb.IsBoolFlag() {
203			// Bool short flag.
204		} else {
205			// Short flag with combined argument: -fARG
206			token := &Token{p.argi, TokenShort, short}
207			if len(arg) > size+1 {
208				p.Push(&Token{p.argi, TokenArg, arg[size+1:]})
209			}
210			return token
211		}
212
213		if len(arg) > size+1 {
214			p.args = append([]string{"-" + arg[size+1:]}, p.args...)
215		}
216		return &Token{p.argi, TokenShort, short}
217	} else if strings.HasPrefix(arg, "@") {
218		expanded, err := ExpandArgsFromFile(arg[1:])
219		if err != nil {
220			return &Token{p.argi, TokenError, err.Error()}
221		}
222		if len(p.args) == 0 {
223			p.args = expanded
224		} else {
225			p.args = append(expanded, p.args...)
226		}
227		return p.Next()
228	}
229
230	return &Token{p.argi, TokenArg, arg}
231}
232
233func (p *ParseContext) Peek() *Token {
234	if len(p.peek) == 0 {
235		return p.Push(p.Next())
236	}
237	return p.peek[len(p.peek)-1]
238}
239
240func (p *ParseContext) Push(token *Token) *Token {
241	p.peek = append(p.peek, token)
242	return token
243}
244
245func (p *ParseContext) pop() *Token {
246	end := len(p.peek) - 1
247	token := p.peek[end]
248	p.peek = p.peek[0:end]
249	return token
250}
251
252func (p *ParseContext) String() string {
253	return p.SelectedCommand.FullCommand()
254}
255
256func (p *ParseContext) matchedFlag(flag *FlagClause, value string) {
257	p.Elements = append(p.Elements, &ParseElement{Clause: flag, Value: &value})
258}
259
260func (p *ParseContext) matchedArg(arg *ArgClause, value string) {
261	p.Elements = append(p.Elements, &ParseElement{Clause: arg, Value: &value})
262}
263
264func (p *ParseContext) matchedCmd(cmd *CmdClause) {
265	p.Elements = append(p.Elements, &ParseElement{Clause: cmd})
266	p.mergeFlags(cmd.flagGroup)
267	p.mergeArgs(cmd.argGroup)
268	p.SelectedCommand = cmd
269}
270
271// Expand arguments from a file. Lines starting with # will be treated as comments.
272func ExpandArgsFromFile(filename string) (out []string, err error) {
273	if filename == "" {
274		return nil, fmt.Errorf("expected @ file to expand arguments from")
275	}
276	r, err := os.Open(filename)
277	if err != nil {
278		return nil, fmt.Errorf("failed to open arguments file %q: %s", filename, err)
279	}
280	defer r.Close()
281	scanner := bufio.NewScanner(r)
282	for scanner.Scan() {
283		line := scanner.Text()
284		if strings.HasPrefix(line, "#") {
285			continue
286		}
287		out = append(out, line)
288	}
289	err = scanner.Err()
290	if err != nil {
291		return nil, fmt.Errorf("failed to read arguments from %q: %s", filename, err)
292	}
293	return
294}
295
296func parse(context *ParseContext, app *Application) (err error) {
297	context.mergeFlags(app.flagGroup)
298	context.mergeArgs(app.argGroup)
299
300	cmds := app.cmdGroup
301	ignoreDefault := context.ignoreDefault
302
303loop:
304	for !context.EOL() && !context.Error() {
305		token := context.Peek()
306
307		switch token.Type {
308		case TokenLong, TokenShort:
309			if flag, err := context.flags.parse(context); err != nil {
310				if !ignoreDefault {
311					if cmd := cmds.defaultSubcommand(); cmd != nil {
312						cmd.completionAlts = cmds.cmdNames()
313						context.matchedCmd(cmd)
314						cmds = cmd.cmdGroup
315						break
316					}
317				}
318				return err
319			} else if flag == HelpFlag {
320				ignoreDefault = true
321			}
322
323		case TokenArg:
324			if cmds.have() {
325				selectedDefault := false
326				cmd, ok := cmds.commands[token.String()]
327				if !ok {
328					if !ignoreDefault {
329						if cmd = cmds.defaultSubcommand(); cmd != nil {
330							cmd.completionAlts = cmds.cmdNames()
331							selectedDefault = true
332						}
333					}
334					if cmd == nil {
335						return fmt.Errorf("expected command but got %q", token)
336					}
337				}
338				if cmd == HelpCommand {
339					ignoreDefault = true
340				}
341				cmd.completionAlts = nil
342				context.matchedCmd(cmd)
343				cmds = cmd.cmdGroup
344				if !selectedDefault {
345					context.Next()
346				}
347			} else if context.arguments.have() {
348				if app.noInterspersed {
349					// no more flags
350					context.argsOnly = true
351				}
352				arg := context.nextArg()
353				if arg == nil {
354					break loop
355				}
356				context.matchedArg(arg, token.String())
357				context.Next()
358			} else {
359				break loop
360			}
361
362		case TokenEOL:
363			break loop
364		}
365	}
366
367	// Move to innermost default command.
368	for !ignoreDefault {
369		if cmd := cmds.defaultSubcommand(); cmd != nil {
370			cmd.completionAlts = cmds.cmdNames()
371			context.matchedCmd(cmd)
372			cmds = cmd.cmdGroup
373		} else {
374			break
375		}
376	}
377
378	if context.Error() {
379		return fmt.Errorf("%s", context.Peek().Value)
380	}
381
382	if !context.EOL() {
383		return fmt.Errorf("unexpected %s", context.Peek())
384	}
385
386	// Set defaults for all remaining args.
387	for arg := context.nextArg(); arg != nil && !arg.consumesRemainder(); arg = context.nextArg() {
388		for _, defaultValue := range arg.defaultValues {
389			if err := arg.value.Set(defaultValue); err != nil {
390				return fmt.Errorf("invalid default value '%s' for argument '%s'", defaultValue, arg.name)
391			}
392		}
393	}
394
395	return
396}
397