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