PageRenderTime 30ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/src/optparse.coffee

http://github.com/jashkenas/coffee-script
CoffeeScript | 165 lines | 102 code | 15 blank | 48 comment | 27 complexity | 3392d54189a9c8b38a0bbd1e9ce78877 MD5 | raw file
  1. {repeat} = require './helpers'
  2. # A simple **OptionParser** class to parse option flags from the command-line.
  3. # Use it like so:
  4. #
  5. # parser = new OptionParser switches, helpBanner
  6. # options = parser.parse process.argv
  7. #
  8. # The first non-option is considered to be the start of the file (and file
  9. # option) list, and all subsequent arguments are left unparsed.
  10. #
  11. # The `coffee` command uses an instance of **OptionParser** to parse its
  12. # command-line arguments in `src/command.coffee`.
  13. exports.OptionParser = class OptionParser
  14. # Initialize with a list of valid options, in the form:
  15. #
  16. # [short-flag, long-flag, description]
  17. #
  18. # Along with an optional banner for the usage help.
  19. constructor: (ruleDeclarations, @banner) ->
  20. @rules = buildRules ruleDeclarations
  21. # Parse the list of arguments, populating an `options` object with all of the
  22. # specified options, and return it. Options after the first non-option
  23. # argument are treated as arguments. `options.arguments` will be an array
  24. # containing the remaining arguments. This is a simpler API than many option
  25. # parsers that allow you to attach callback actions for every flag. Instead,
  26. # you're responsible for interpreting the options object.
  27. parse: (args) ->
  28. # The CoffeeScript option parser is a little odd; options after the first
  29. # non-option argument are treated as non-option arguments themselves.
  30. # Optional arguments are normalized by expanding merged flags into multiple
  31. # flags. This allows you to have `-wl` be the same as `--watch --lint`.
  32. # Note that executable scripts with a shebang (`#!`) line should use the
  33. # line `#!/usr/bin/env coffee`, or `#!/absolute/path/to/coffee`, without a
  34. # `--` argument after, because that will fail on Linux (see #3946).
  35. {rules, positional} = normalizeArguments args, @rules.flagDict
  36. options = {}
  37. # The `argument` field is added to the rule instance non-destructively by
  38. # `normalizeArguments`.
  39. for {hasArgument, argument, isList, name} in rules
  40. if hasArgument
  41. if isList
  42. options[name] ?= []
  43. options[name].push argument
  44. else
  45. options[name] = argument
  46. else
  47. options[name] = true
  48. if positional[0] is '--'
  49. options.doubleDashed = yes
  50. positional = positional[1..]
  51. options.arguments = positional
  52. options
  53. # Return the help text for this **OptionParser**, listing and describing all
  54. # of the valid options, for `--help` and such.
  55. help: ->
  56. lines = []
  57. lines.unshift "#{@banner}\n" if @banner
  58. for rule in @rules.ruleList
  59. spaces = 15 - rule.longFlag.length
  60. spaces = if spaces > 0 then repeat ' ', spaces else ''
  61. letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
  62. lines.push ' ' + letPart + rule.longFlag + spaces + rule.description
  63. "\n#{ lines.join('\n') }\n"
  64. # Helpers
  65. # -------
  66. # Regex matchers for option flags on the command line and their rules.
  67. LONG_FLAG = /^(--\w[\w\-]*)/
  68. SHORT_FLAG = /^(-\w)$/
  69. MULTI_FLAG = /^-(\w{2,})/
  70. # Matches the long flag part of a rule for an option with an argument. Not
  71. # applied to anything in process.argv.
  72. OPTIONAL = /\[(\w+(\*?))\]/
  73. # Build and return the list of option rules. If the optional *short-flag* is
  74. # unspecified, leave it out by padding with `null`.
  75. buildRules = (ruleDeclarations) ->
  76. ruleList = for tuple in ruleDeclarations
  77. tuple.unshift null if tuple.length < 3
  78. buildRule tuple...
  79. flagDict = {}
  80. for rule in ruleList
  81. # `shortFlag` is null if not provided in the rule.
  82. for flag in [rule.shortFlag, rule.longFlag] when flag?
  83. if flagDict[flag]?
  84. throw new Error "flag #{flag} for switch #{rule.name}
  85. was already declared for switch #{flagDict[flag].name}"
  86. flagDict[flag] = rule
  87. {ruleList, flagDict}
  88. # Build a rule from a `-o` short flag, a `--output [DIR]` long flag, and the
  89. # description of what the option does.
  90. buildRule = (shortFlag, longFlag, description) ->
  91. match = longFlag.match(OPTIONAL)
  92. shortFlag = shortFlag?.match(SHORT_FLAG)[1]
  93. longFlag = longFlag.match(LONG_FLAG)[1]
  94. {
  95. name: longFlag.replace /^--/, ''
  96. shortFlag: shortFlag
  97. longFlag: longFlag
  98. description: description
  99. hasArgument: !!(match and match[1])
  100. isList: !!(match and match[2])
  101. }
  102. normalizeArguments = (args, flagDict) ->
  103. rules = []
  104. positional = []
  105. needsArgOpt = null
  106. for arg, argIndex in args
  107. # If the previous argument given to the script was an option that uses the
  108. # next command-line argument as its argument, create copy of the options
  109. # rule with an `argument` field.
  110. if needsArgOpt?
  111. withArg = Object.assign {}, needsArgOpt.rule, {argument: arg}
  112. rules.push withArg
  113. needsArgOpt = null
  114. continue
  115. multiFlags = arg.match(MULTI_FLAG)?[1]
  116. .split('')
  117. .map (flagName) -> "-#{flagName}"
  118. if multiFlags?
  119. multiOpts = multiFlags.map (flag) ->
  120. rule = flagDict[flag]
  121. unless rule?
  122. throw new Error "unrecognized option #{flag} in multi-flag #{arg}"
  123. {rule, flag}
  124. # Only the last flag in a multi-flag may have an argument.
  125. [innerOpts..., lastOpt] = multiOpts
  126. for {rule, flag} in innerOpts
  127. if rule.hasArgument
  128. throw new Error "cannot use option #{flag} in multi-flag #{arg} except
  129. as the last option, because it needs an argument"
  130. rules.push rule
  131. if lastOpt.rule.hasArgument
  132. needsArgOpt = lastOpt
  133. else
  134. rules.push lastOpt.rule
  135. else if ([LONG_FLAG, SHORT_FLAG].some (pat) -> arg.match(pat)?)
  136. singleRule = flagDict[arg]
  137. unless singleRule?
  138. throw new Error "unrecognized option #{arg}"
  139. if singleRule.hasArgument
  140. needsArgOpt = {rule: singleRule, flag: arg}
  141. else
  142. rules.push singleRule
  143. else
  144. # This is a positional argument.
  145. positional = args[argIndex..]
  146. break
  147. if needsArgOpt?
  148. throw new Error "value required for #{needsArgOpt.flag}, but it was the last
  149. argument provided"
  150. {rules, positional}