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