dreamopt.coffee 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. wordwrap = require 'wordwrap'
  2. USAGE = /^Usage:/
  3. HEADER = /^[^-].*:$/
  4. OPTION = /^\s+-/
  5. COMMAND = ///^ \s+ (\w+) (?: \s{2,} (\S.*) ) $///
  6. ARGUMENT = /// ^ \s+ .* \s\s | ^ \s+ \S+ $ ///
  7. TEXT = /^\S/
  8. # if only JavaScript had a sane split(), we'd skip some of these
  9. OPTION_DESC = ///^ (.*?) \s{2,} (.*) $///
  10. OPTION_METAVARS = ///^ ([^\s,]+ (?:,\s* \S+)? ) \s+ ([^,].*) $///
  11. OPTION_SHORT = ///^ (-\S) (?: , \s* (.*) )? $///
  12. OPTION_LONG = ///^ (--\S+) $///
  13. OPTION_BOOL = ///^ --\[no-\](.*) $///
  14. OPTION_DESC_TAG = ///^ (.*) \#(\w+) (?: \( ([^()]*) \) )? \s* $///
  15. DUMMY = /// \# /// # make Sublime Text syntax highlighting happy
  16. OPTION_DESC_DEFAULT = /// \( (?: default: | default\s+is | defaults\s+to ) \s+ ([^()]+) \) ///i
  17. DefaultHandlers =
  18. auto: (value) ->
  19. return value unless typeof value is 'string'
  20. return Number(value) if not isNaN(Number(value))
  21. return value
  22. string: (value) -> value
  23. int: (value) ->
  24. return value unless typeof value is 'string'
  25. if isNaN(parseInt(value, 10))
  26. throw new Error("Integer value required: #{value}")
  27. return parseInt(value, 10)
  28. flag: (value, options, optionName, tagValue) ->
  29. return yes if !value?
  30. return value if typeof value isnt 'string'
  31. return no if value.toLowerCase() in ['0', 'false', 'no', 'off']
  32. return yes if value.toLowerCase() in ['', '1', 'true', 'yes', 'on']
  33. throw new Error("Invalid flag value #{JSON.stringify(value)} for option #{optionName}")
  34. alignment = 24
  35. indent = " "
  36. separator = " "
  37. width = 100
  38. wrapText = require('wordwrap')(width)
  39. formatUsageString = (left, right) ->
  40. overhead = indent.length + separator.length
  41. if left.length < alignment - overhead
  42. padding = new Array(alignment - overhead - left.length + 1).join(' ')
  43. else
  44. padding = ''
  45. actualAlignment = overhead + left.length + padding.length
  46. descriptionWidth = width - actualAlignment
  47. wrappedLineIndent = new Array(actualAlignment + 1).join(' ')
  48. [firstLine, otherLines...] = wordwrap(descriptionWidth)(right).trim().split('\n')
  49. right = [firstLine].concat(otherLines.map (line) -> wrappedLineIndent + line).join("\n")
  50. right += "\n" if otherLines.length
  51. return " #{left}#{padding} #{right}"
  52. class Option
  53. constructor: (@shortOpt, @longOpt, @desc, tagPairs, @metavars, @defaultValue) ->
  54. if @longOpt || @shortOpt
  55. @name = @longOpt && @longOpt.slice(2) || @shortOpt.slice(1)
  56. else if @metavars.length
  57. @name = @metavars[0]
  58. if $ = @name.match ///^ \[ (.*) \] $///
  59. @name = $[1]
  60. @var = @name
  61. @tags = {}
  62. @tagsOrder = []
  63. for [tag, value] in tagPairs
  64. @tags[tag] = value
  65. @tagsOrder.push tag
  66. switch tag
  67. when 'default' then @defaultValue = value
  68. when 'var' then @var = value
  69. @func = null
  70. leftUsageComponent: ->
  71. longOpt = @longOpt
  72. if longOpt && @tags.acceptsno
  73. longOpt = "--[no-]" + longOpt.slice(2)
  74. string = switch
  75. when @shortOpt and longOpt then "#{@shortOpt}, #{longOpt}"
  76. when @shortOpt then @shortOpt
  77. when @longOpt then " #{longOpt}"
  78. else ''
  79. if @metavars
  80. string = string + (string && ' ' || '') + @metavars.join(' ')
  81. return string
  82. toUsageString: -> formatUsageString(@leftUsageComponent(), @desc)
  83. coerce: (value, options, syntax) ->
  84. any = no
  85. for tag in @tagsOrder
  86. if handler = (syntax.handlers[tag] || DefaultHandlers[tag])
  87. newValue = handler(value, options, @leftUsageComponent(), @tags[tag])
  88. unless typeof newValue is undefined
  89. value = newValue
  90. any = yes
  91. unless any
  92. value = DefaultHandlers.auto(value, options, syntax, @leftUsageComponent())
  93. return value
  94. class Command
  95. constructor: (@name, @desc, @syntax) ->
  96. @func = null
  97. leftUsageComponent: -> @name
  98. toUsageString: -> formatUsageString(@leftUsageComponent(), @desc)
  99. class Syntax
  100. constructor: (@handlers, specs=[]) ->
  101. @usage = []
  102. @options = []
  103. @arguments = []
  104. @commands = {}
  105. @commandsOrder = []
  106. @shortOptions = {}
  107. @longOptions = {}
  108. @usageFound = no
  109. @headerAdded = no
  110. @implicitHeaders =
  111. options: "Options:"
  112. arguments: "Arguments:"
  113. commands: "Commands:"
  114. @lastSectionType = 'none'
  115. @customHeaderAdded = no
  116. if specs
  117. @add(specs)
  118. addHeader: (header) ->
  119. @usage.push "\n#{header}"
  120. @lastSectionType = 'any'
  121. ensureHeaderExists: (sectionType) ->
  122. if @lastSectionType is 'any'
  123. @lastSectionType = sectionType
  124. else if @lastSectionType != sectionType
  125. @addHeader @implicitHeaders[sectionType]
  126. @lastSectionType = sectionType
  127. add: (specs) ->
  128. unless typeof specs is 'object'
  129. specs = [specs]
  130. specs = specs.slice(0)
  131. gotArray = -> (typeof specs[0] is 'object') and (specs[0] instanceof Array)
  132. gotFunction = -> typeof specs[0] is 'function'
  133. while spec = specs.shift()
  134. if typeof spec != 'string'
  135. throw new Error("Expected string spec, found #{typeof spec}")
  136. if spec.match(HEADER)
  137. @addHeader spec
  138. else if spec.match(USAGE)
  139. @usage.unshift "#{spec}"
  140. @usageFound = yes
  141. else if spec.match(OPTION)
  142. @options.push (option = Option.parse(spec.trim()))
  143. @shortOptions[option.shortOpt.slice(1)] = option if option.shortOpt
  144. @longOptions[option.longOpt.slice(2)] = option if option.longOpt
  145. if gotFunction()
  146. option.func = specs.shift()
  147. @ensureHeaderExists 'options'
  148. @usage.push option.toUsageString()
  149. else if !gotArray() and spec.match(ARGUMENT)
  150. @arguments.push (option = Option.parse(spec.trim()))
  151. if gotFunction()
  152. option.func = specs.shift()
  153. @ensureHeaderExists 'arguments'
  154. @usage.push option.toUsageString()
  155. else if $ = spec.match COMMAND
  156. [name, desc] = $
  157. unless gotArray()
  158. throw new Error("Array must follow a command spec: #{JSON.stringify(spec)}")
  159. subsyntax = new Syntax(@handlers, specs.shift())
  160. @commands[name] = command = new Command(name, desc, subsyntax)
  161. @commandsOrder.push name
  162. @ensureHeaderExists 'commands'
  163. @usage.push command.toUsageString()
  164. else if spec.match TEXT
  165. @usage.push "\n" + wrapText(spec.trim())
  166. else
  167. throw new Error("String spec invalid: #{JSON.stringify(spec)}")
  168. return this
  169. toUsageString: -> (line + "\n" for line in @usage).join('')
  170. parse: (argv) ->
  171. argv = argv.slice(0)
  172. result = {}
  173. positional = []
  174. funcs = []
  175. executeHook = (option, value) =>
  176. if option.func
  177. if option.tags.delayfunc
  178. funcs.push [option.func, option, value]
  179. else
  180. newValue = option.func(value, result, this, option)
  181. if newValue?
  182. value = newValue
  183. return value
  184. processOption = (result, arg, option, value) =>
  185. switch option.metavars.length
  186. when 0
  187. value = true
  188. when 1
  189. value ?= argv.shift()
  190. if typeof value is 'undefined'
  191. throw new Error("Option #{arg} requires an argument: #{option.leftUsageComponent()}")
  192. else
  193. value = []
  194. for metavar, index in option.metavars
  195. value.push (subvalue = argv.shift())
  196. if typeof subvalue is 'undefined'
  197. throw new Error("Option #{arg} requires #{option.metavars.length} arguments: #{option.leftUsageComponent()}")
  198. return option.coerce(value, result, this)
  199. assignValue = (result, option, value) =>
  200. if option.tags.list
  201. if not result.hasOwnProperty(option.var)
  202. result[option.var] = []
  203. if value?
  204. result[option.var].push(value)
  205. else
  206. result[option.var] = value
  207. while arg = argv.shift()
  208. if arg is '--'
  209. while arg = argv.shift()
  210. positional.push arg
  211. else if arg is '-'
  212. positional.push arg
  213. else if arg.match(/^--no-/) && (option = @longOptions[arg.slice(5)]) && option.tags.flag
  214. assignValue result, option, false
  215. else if $ = arg.match(///^ -- ([^=]+) (?: = (.*) )? $///)
  216. [_, name, value] = $
  217. if option = @longOptions[name]
  218. value = processOption(result, arg, option, value)
  219. value = executeHook(option, value)
  220. assignValue result, option, value
  221. else
  222. throw new Error("Unknown long option: #{arg}")
  223. else if arg.match /^-/
  224. remainder = arg.slice(1)
  225. while remainder
  226. subarg = remainder[0]
  227. remainder = remainder.slice(1)
  228. if option = @shortOptions[subarg]
  229. if remainder && option.metavars.length > 0
  230. value = remainder
  231. remainder = ''
  232. else
  233. value = undefined
  234. value = processOption(result, arg, option, value)
  235. value = executeHook(option, value)
  236. assignValue result, option, value
  237. else
  238. if arg == "-#{subarg}"
  239. throw new Error("Unknown short option #{arg}")
  240. else
  241. throw new Error("Unknown short option -#{subarg} in #{arg}")
  242. else
  243. positional.push arg
  244. for option in @options
  245. if !result.hasOwnProperty(option.var)
  246. if option.tags.required
  247. throw new Error("Missing required option: #{option.leftUsageComponent()}")
  248. if option.defaultValue? or option.tags.fancydefault or option.tags.list
  249. if option.defaultValue?
  250. value = option.coerce(option.defaultValue, result, this)
  251. else
  252. value = null
  253. value = executeHook(option, value)
  254. assignValue result, option, value
  255. for arg, index in positional
  256. if option = @arguments[index]
  257. value = option.coerce(arg, result, this)
  258. value = executeHook(option, value)
  259. positional[index] = value
  260. if option.var
  261. assignValue result, option, value
  262. for option, index in @arguments
  263. if index >= positional.length
  264. if option.tags.required
  265. throw new Error("Missing required argument \##{index + 1}: #{option.leftUsageComponent()}")
  266. if option.defaultValue? or option.tags.fancydefault
  267. if option.defaultValue?
  268. value = option.coerce(option.defaultValue, result, this)
  269. else
  270. value = null
  271. value = executeHook(option, value)
  272. if option.var
  273. assignValue result, option, value
  274. if index == positional.length
  275. positional.push value
  276. else if !option.var && !option.func
  277. 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")
  278. result.argv = positional
  279. for [func, option, value] in funcs
  280. func(value, result, this, option)
  281. return result
  282. Option.parse = (spec) ->
  283. isOption = (' ' + spec).match(OPTION)
  284. [_, options, desc] = spec.match(OPTION_DESC) || [undefined, spec, ""]
  285. if isOption
  286. [_, options, metavars] = options.match(OPTION_METAVARS) || [undefined, options, ""]
  287. [_, shortOpt, options] = options.match(OPTION_SHORT) || [undefined, "", options]
  288. [_, longOpt, options] = (options || '').match(OPTION_LONG) || [undefined, "", options]
  289. else
  290. [metavars, options] = [options, ""]
  291. metavars = metavars && metavars.split(/\s+/) || []
  292. tags = (([_, desc, tag, value] = $; [tag, value ? true]) while $ = desc.match(OPTION_DESC_TAG))
  293. tags.reverse()
  294. if longOpt && longOpt.match(OPTION_BOOL)
  295. tags.push ['acceptsno', true]
  296. longOpt = longOpt.replace('--[no-]', '--')
  297. if isOption && metavars.length == 0
  298. tags.push ['flag', true]
  299. if $ = desc.match(OPTION_DESC_DEFAULT)
  300. defaultValue = $[1]
  301. if defaultValue.match(/\s/)
  302. # the default is too fancy, don't use it verbatim, but call the user callback if any to obtain the default
  303. defaultValue = undefined
  304. tags.push ['fancydefault', true]
  305. if options
  306. throw new Error("Invalid option spec format (cannot parse #{JSON.stringify(options)}): #{JSON.stringify(spec)}")
  307. if isOption && !(shortOpt || longOpt)
  308. throw new Error("Invalid option spec format !(shortOpt || longOpt): #{JSON.stringify(spec)}")
  309. new Option(shortOpt || null, longOpt || null, desc.trim(), tags, metavars, defaultValue)
  310. printUsage = (usage) ->
  311. console.error(usage)
  312. process.exit 1
  313. handleUsage = (printUsage, value, options, syntax) ->
  314. printUsage syntax.toUsageString()
  315. parse = (specs, handlers, argv) ->
  316. if !argv? and (handlers instanceof Array)
  317. argv = handlers
  318. handlers = {}
  319. handlers ?= {}
  320. argv ?= process.argv.slice(2)
  321. syntax = new Syntax(handlers, specs)
  322. unless syntax.longOptions.help
  323. syntax.add [" -h, --help Display this usage information", (v, o, s) -> handleUsage(handlers.printUsage ? printUsage, v, o, s)]
  324. syntax.parse(argv)
  325. module.exports = parse
  326. # for testing
  327. module.exports.parseOptionSpec = Option.parse
  328. module.exports.Syntax = Syntax