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