| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 | 
							- wordwrap = require 'wordwrap'
 
- USAGE    = /^Usage:/
 
- HEADER   = /^[^-].*:$/
 
- OPTION   = /^\s+-/
 
- COMMAND  = ///^  \s+  (\w+)  (?: \s{2,} (\S.*) )  $///
 
- ARGUMENT = /// ^ \s+ .* \s\s | ^ \s+ \S+ $ ///
 
- TEXT     = /^\S/
 
- # if only JavaScript had a sane split(), we'd skip some of these
 
- OPTION_DESC         = ///^ (.*?)  \s{2,}  (.*) $///
 
- OPTION_METAVARS     = ///^ ([^\s,]+ (?:,\s* \S+)? )  \s+  ([^,].*) $///
 
- OPTION_SHORT        = ///^ (-\S)  (?: , \s* (.*) )? $///
 
- OPTION_LONG         = ///^ (--\S+) $///
 
- OPTION_BOOL         = ///^ --\[no-\](.*) $///
 
- OPTION_DESC_TAG     = ///^ (.*) \#(\w+) (?: \(  ([^()]*)  \) )? \s* $///
 
- DUMMY               = /// \# ///   # make Sublime Text syntax highlighting happy
 
- OPTION_DESC_DEFAULT = ///  \(  (?: default: | default\s+is | defaults\s+to )  \s+  ([^()]+)  \)  ///i
 
- DefaultHandlers =
 
-   auto: (value) ->
 
-     return value unless typeof value is 'string'
 
-     return Number(value) if not isNaN(Number(value))
 
-     return value
 
-   string: (value) -> value
 
-   int: (value) ->
 
-     return value unless typeof value is 'string'
 
-     if isNaN(parseInt(value, 10))
 
-       throw new Error("Integer value required: #{value}")
 
-     return parseInt(value, 10)
 
-   flag: (value, options, optionName, tagValue) ->
 
-     return yes   if !value?
 
-     return value if typeof value isnt 'string'
 
-     return no    if value.toLowerCase() in ['0', 'false', 'no', 'off']
 
-     return yes   if value.toLowerCase() in ['', '1', 'true', 'yes', 'on']
 
-     throw new Error("Invalid flag value #{JSON.stringify(value)} for option #{optionName}")
 
- alignment   = 24
 
- indent      = "  "
 
- separator   = "  "
 
- width       = 100
 
- wrapText        = require('wordwrap')(width)
 
- formatUsageString = (left, right) ->
 
-   overhead = indent.length + separator.length
 
-   if left.length < alignment - overhead
 
-     padding = new Array(alignment - overhead - left.length + 1).join(' ')
 
-   else
 
-     padding = ''
 
-   actualAlignment   = overhead + left.length + padding.length
 
-   descriptionWidth  = width - actualAlignment
 
-   wrappedLineIndent = new Array(actualAlignment + 1).join(' ')
 
-   [firstLine, otherLines...] = wordwrap(descriptionWidth)(right).trim().split('\n')
 
-   right = [firstLine].concat(otherLines.map (line) -> wrappedLineIndent + line).join("\n")
 
-   right += "\n" if otherLines.length
 
-   return "  #{left}#{padding}  #{right}"
 
- class Option
 
-   constructor: (@shortOpt, @longOpt, @desc, tagPairs, @metavars, @defaultValue) ->
 
-     if @longOpt || @shortOpt
 
-       @name = @longOpt && @longOpt.slice(2) || @shortOpt.slice(1)
 
-     else if @metavars.length
 
-       @name = @metavars[0]
 
-       if $ = @name.match ///^ \[ (.*) \] $///
 
-         @name = $[1]
 
-     @var = @name
 
-     @tags = {}
 
-     @tagsOrder = []
 
-     for [tag, value] in tagPairs
 
-       @tags[tag] = value
 
-       @tagsOrder.push tag
 
-       switch tag
 
-         when 'default' then @defaultValue = value
 
-         when 'var'     then @var = value
 
-     @func = null
 
-   leftUsageComponent: ->
 
-     longOpt = @longOpt
 
-     if longOpt && @tags.acceptsno
 
-       longOpt = "--[no-]" + longOpt.slice(2)
 
-     string = switch
 
-       when @shortOpt and longOpt then "#{@shortOpt}, #{longOpt}"
 
-       when @shortOpt             then @shortOpt
 
-       when @longOpt              then "    #{longOpt}"
 
-       else ''
 
-     if @metavars
 
-       string = string + (string && ' ' || '') + @metavars.join(' ')
 
-     return string
 
-   toUsageString: -> formatUsageString(@leftUsageComponent(), @desc)
 
-   coerce: (value, options, syntax) ->
 
-     any = no
 
-     for tag in @tagsOrder
 
-       if handler = (syntax.handlers[tag] || DefaultHandlers[tag])
 
-         newValue = handler(value, options, @leftUsageComponent(), @tags[tag])
 
-         unless typeof newValue is undefined
 
-           value = newValue
 
-         any = yes
 
-     unless any
 
-       value = DefaultHandlers.auto(value, options, syntax, @leftUsageComponent())
 
-     return value
 
- class Command
 
-   constructor: (@name, @desc, @syntax) ->
 
-     @func = null
 
-   leftUsageComponent: -> @name
 
-   toUsageString: -> formatUsageString(@leftUsageComponent(), @desc)
 
- class Syntax
 
-   constructor: (@handlers, specs=[]) ->
 
-     @usage     = []
 
-     @options   = []
 
-     @arguments = []
 
-     @commands  = {}
 
-     @commandsOrder = []
 
-     @shortOptions = {}
 
-     @longOptions  = {}
 
-     @usageFound = no
 
-     @headerAdded = no
 
-     @implicitHeaders =
 
-       options:   "Options:"
 
-       arguments: "Arguments:"
 
-       commands:  "Commands:"
 
-     @lastSectionType = 'none'
 
-     @customHeaderAdded = no
 
-     if specs
 
-       @add(specs)
 
-   addHeader: (header) ->
 
-     @usage.push "\n#{header}"
 
-     @lastSectionType = 'any'
 
-   ensureHeaderExists: (sectionType) ->
 
-     if @lastSectionType is 'any'
 
-       @lastSectionType = sectionType
 
-     else if @lastSectionType != sectionType
 
-       @addHeader @implicitHeaders[sectionType]
 
-       @lastSectionType = sectionType
 
-   add: (specs) ->
 
-     unless typeof specs is 'object'
 
-       specs = [specs]
 
-     specs = specs.slice(0)
 
-     gotArray    = -> (typeof specs[0] is 'object') and (specs[0] instanceof Array)
 
-     gotFunction = -> typeof specs[0] is 'function'
 
-     while spec = specs.shift()
 
-       if typeof spec != 'string'
 
-         throw new Error("Expected string spec, found #{typeof spec}")
 
-       if spec.match(HEADER)
 
-         @addHeader spec
 
-       else if spec.match(USAGE)
 
-         @usage.unshift "#{spec}"
 
-         @usageFound = yes
 
-       else if spec.match(OPTION)
 
-         @options.push (option = Option.parse(spec.trim()))
 
-         @shortOptions[option.shortOpt.slice(1)] = option  if option.shortOpt
 
-         @longOptions[option.longOpt.slice(2)]   = option  if option.longOpt
 
-         if gotFunction()
 
-           option.func = specs.shift()
 
-         @ensureHeaderExists 'options'
 
-         @usage.push option.toUsageString()
 
-       else if !gotArray() and spec.match(ARGUMENT)
 
-         @arguments.push (option = Option.parse(spec.trim()))
 
-         if gotFunction()
 
-           option.func = specs.shift()
 
-         @ensureHeaderExists 'arguments'
 
-         @usage.push option.toUsageString()
 
-       else if $ = spec.match COMMAND
 
-         [name, desc] = $
 
-         unless gotArray()
 
-           throw new Error("Array must follow a command spec: #{JSON.stringify(spec)}")
 
-         subsyntax = new Syntax(@handlers, specs.shift())
 
-         @commands[name] = command = new Command(name, desc, subsyntax)
 
-         @commandsOrder.push name
 
-         @ensureHeaderExists 'commands'
 
-         @usage.push command.toUsageString()
 
-       else if spec.match TEXT
 
-         @usage.push "\n" + wrapText(spec.trim())
 
-       else
 
-         throw new Error("String spec invalid: #{JSON.stringify(spec)}")
 
-     return this
 
-   toUsageString: -> (line + "\n" for line in @usage).join('')
 
-   parse: (argv) ->
 
-     argv = argv.slice(0)
 
-     result     = {}
 
-     positional = []
 
-     funcs      = []
 
-     executeHook = (option, value) =>
 
-       if option.func
 
-         if option.tags.delayfunc
 
-           funcs.push [option.func, option, value]
 
-         else
 
-           newValue = option.func(value, result, this, option)
 
-           if newValue?
 
-             value = newValue
 
-       return value
 
-     processOption = (result, arg, option, value) =>
 
-       switch option.metavars.length
 
-         when 0
 
-           value = true
 
-         when 1
 
-           value ?= argv.shift()
 
-           if typeof value is 'undefined'
 
-             throw new Error("Option #{arg} requires an argument: #{option.leftUsageComponent()}")
 
-         else
 
-           value = []
 
-           for metavar, index in option.metavars
 
-             value.push (subvalue = argv.shift())
 
-             if typeof subvalue is 'undefined'
 
-               throw new Error("Option #{arg} requires #{option.metavars.length} arguments: #{option.leftUsageComponent()}")
 
-       return option.coerce(value, result, this)
 
-     assignValue = (result, option, value) =>
 
-       if option.tags.list
 
-         if not result.hasOwnProperty(option.var)
 
-           result[option.var] = []
 
-         if value?
 
-           result[option.var].push(value)
 
-       else
 
-         result[option.var] = value
 
-     while arg = argv.shift()
 
-       if arg is '--'
 
-         while arg = argv.shift()
 
-           positional.push arg
 
-       else if arg is '-'
 
-         positional.push arg
 
-       else if arg.match(/^--no-/) && (option = @longOptions[arg.slice(5)]) && option.tags.flag
 
-         assignValue result, option, false
 
-       else if $ = arg.match(///^  --  ([^=]+)  (?: = (.*) )?  $///)
 
-         [_, name, value] = $
 
-         if option = @longOptions[name]
 
-           value = processOption(result, arg, option, value)
 
-           value = executeHook(option, value)
 
-           assignValue result, option, value
 
-         else
 
-           throw new Error("Unknown long option: #{arg}")
 
-       else if arg.match /^-/
 
-         remainder = arg.slice(1)
 
-         while remainder
 
-           subarg    = remainder[0]
 
-           remainder = remainder.slice(1)
 
-           if option = @shortOptions[subarg]
 
-             if remainder && option.metavars.length > 0
 
-               value = remainder
 
-               remainder = ''
 
-             else
 
-               value = undefined
 
-             value = processOption(result, arg, option, value)
 
-             value = executeHook(option, value)
 
-             assignValue result, option, value
 
-           else
 
-             if arg == "-#{subarg}"
 
-               throw new Error("Unknown short option #{arg}")
 
-             else
 
-               throw new Error("Unknown short option -#{subarg} in #{arg}")
 
-       else
 
-         positional.push arg
 
-     for option in @options
 
-       if !result.hasOwnProperty(option.var)
 
-         if option.tags.required
 
-           throw new Error("Missing required option: #{option.leftUsageComponent()}")
 
-         if option.defaultValue? or option.tags.fancydefault or option.tags.list
 
-           if option.defaultValue?
 
-             value = option.coerce(option.defaultValue, result, this)
 
-           else
 
-             value = null
 
-           value = executeHook(option, value)
 
-           assignValue result, option, value
 
-     for arg, index in positional
 
-       if option = @arguments[index]
 
-         value = option.coerce(arg, result, this)
 
-         value = executeHook(option, value)
 
-         positional[index] = value
 
-         if option.var
 
-           assignValue result, option, value
 
-     for option, index in @arguments
 
-       if index >= positional.length
 
-         if option.tags.required
 
-           throw new Error("Missing required argument \##{index + 1}: #{option.leftUsageComponent()}")
 
-         if option.defaultValue? or option.tags.fancydefault
 
-           if option.defaultValue?
 
-             value = option.coerce(option.defaultValue, result, this)
 
-           else
 
-             value = null
 
-           value = executeHook(option, value)
 
-           if option.var
 
-             assignValue result, option, value
 
-           if index == positional.length
 
-             positional.push value
 
-           else if !option.var && !option.func
 
-             throw new Error("Cannot apply default value to argument \##{index + 1} (#{option.leftUsageComponent()}) because no #var is specified, no func is provided and previous arguments don't have default values")
 
-     result.argv = positional
 
-     for [func, option, value] in funcs
 
-       func(value, result, this, option)
 
-     return result
 
- Option.parse = (spec) ->
 
-   isOption = (' ' + spec).match(OPTION)
 
-   [_, options, desc]     = spec.match(OPTION_DESC)     || [undefined, spec, ""]
 
-   if isOption
 
-     [_, options, metavars] = options.match(OPTION_METAVARS) || [undefined, options, ""]
 
-     [_, shortOpt, options] = options.match(OPTION_SHORT) || [undefined, "", options]
 
-     [_, longOpt, options]  = (options || '').match(OPTION_LONG)  || [undefined, "", options]
 
-   else
 
-     [metavars, options] = [options, ""]
 
-   metavars = metavars && metavars.split(/\s+/) || []
 
-   tags = (([_, desc, tag, value] = $; [tag, value ? true]) while $ = desc.match(OPTION_DESC_TAG))
 
-   tags.reverse()
 
-   if longOpt && longOpt.match(OPTION_BOOL)
 
-     tags.push ['acceptsno', true]
 
-     longOpt = longOpt.replace('--[no-]', '--')
 
-   if isOption && metavars.length == 0
 
-     tags.push ['flag', true]
 
-   if $ = desc.match(OPTION_DESC_DEFAULT)
 
-     defaultValue = $[1]
 
-     if defaultValue.match(/\s/)
 
-       # the default is too fancy, don't use it verbatim, but call the user callback if any to obtain the default
 
-       defaultValue = undefined
 
-       tags.push ['fancydefault', true]
 
-   if options
 
-     throw new Error("Invalid option spec format (cannot parse #{JSON.stringify(options)}): #{JSON.stringify(spec)}")
 
-   if isOption && !(shortOpt || longOpt)
 
-     throw new Error("Invalid option spec format !(shortOpt || longOpt): #{JSON.stringify(spec)}")
 
-   new Option(shortOpt || null, longOpt || null, desc.trim(), tags, metavars, defaultValue)
 
- printUsage = (usage) ->
 
-   console.error(usage)
 
-   process.exit 1
 
- handleUsage = (printUsage, value, options, syntax) ->
 
-   printUsage syntax.toUsageString()
 
- parse = (specs, handlers, argv) ->
 
-   if !argv? and (handlers instanceof Array)
 
-     argv = handlers
 
-     handlers = {}
 
-   handlers ?= {}
 
-   argv ?= process.argv.slice(2)
 
-   syntax = new Syntax(handlers, specs)
 
-   unless syntax.longOptions.help
 
-     syntax.add ["  -h, --help  Display this usage information", (v, o, s) -> handleUsage(handlers.printUsage ? printUsage, v, o, s)]
 
-   syntax.parse(argv)
 
- module.exports = parse
 
- # for testing
 
- module.exports.parseOptionSpec = Option.parse
 
- module.exports.Syntax = Syntax
 
 
  |