cmd.coffee 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. UTIL = require 'util'
  2. PATH = require 'path'
  3. Color = require('./color').Color
  4. Q = require('q')
  5. #inspect = require('eyes').inspector { maxLength: 99999, stream: process.stderr }
  6. ###*
  7. Command
  8. Top level entity. Commands may have options and arguments.
  9. @namespace
  10. @class Presents command
  11. ###
  12. exports.Cmd = class Cmd
  13. ###*
  14. @constructs
  15. @param {COA.Cmd} [cmd] parent command
  16. ###
  17. constructor: (cmd) ->
  18. if this not instanceof Cmd
  19. return new Cmd cmd
  20. @_parent cmd
  21. @_cmds = []
  22. @_cmdsByName = {}
  23. @_opts = []
  24. @_optsByKey = {}
  25. @_args = []
  26. @_ext = false
  27. @get: (propertyName, func) ->
  28. Object.defineProperty @::, propertyName,
  29. configurable: true
  30. enumerable: true
  31. get: func
  32. ###*
  33. Returns object containing all its subcommands as methods
  34. to use from other programs.
  35. @returns {Object}
  36. ###
  37. @get 'api', () ->
  38. if not @_api
  39. @_api = => @invoke.apply @, arguments
  40. for c of @_cmdsByName
  41. do (c) =>
  42. @_api[c] = @_cmdsByName[c].api
  43. @_api
  44. _parent: (cmd) ->
  45. @_cmd = cmd or this
  46. if cmd
  47. cmd._cmds.push @
  48. if @_name then @_cmd._cmdsByName[@_name] = @
  49. @
  50. ###*
  51. Set a canonical command identifier to be used anywhere in the API.
  52. @param {String} _name command name
  53. @returns {COA.Cmd} this instance (for chainability)
  54. ###
  55. name: (@_name) ->
  56. if @_cmd isnt @ then @_cmd._cmdsByName[_name] = @
  57. @
  58. ###*
  59. Set a long description for command to be used anywhere in text messages.
  60. @param {String} _title command title
  61. @returns {COA.Cmd} this instance (for chainability)
  62. ###
  63. title: (@_title) -> @
  64. ###*
  65. Create new or add existing subcommand for current command.
  66. @param {COA.Cmd} [cmd] existing command instance
  67. @returns {COA.Cmd} new subcommand instance
  68. ###
  69. cmd: (cmd) ->
  70. if cmd then cmd._parent @
  71. else new Cmd @
  72. ###*
  73. Create option for current command.
  74. @returns {COA.Opt} new option instance
  75. ###
  76. opt: -> new (require('./opt').Opt) @
  77. ###*
  78. Create argument for current command.
  79. @returns {COA.Opt} new argument instance
  80. ###
  81. arg: -> new (require('./arg').Arg) @
  82. ###*
  83. Add (or set) action for current command.
  84. @param {Function} act action function,
  85. invoked in the context of command instance
  86. and has the parameters:
  87. - {Object} opts parsed options
  88. - {Array} args parsed arguments
  89. - {Object} res actions result accumulator
  90. It can return rejected promise by Cmd.reject (in case of error)
  91. or any other value treated as result.
  92. @param {Boolean} [force=false] flag for set action instead add to existings
  93. @returns {COA.Cmd} this instance (for chainability)
  94. ###
  95. act: (act, force) ->
  96. return @ unless act
  97. if not force and @_act
  98. @_act.push act
  99. else
  100. @_act = [act]
  101. @
  102. ###*
  103. Set custom additional completion for current command.
  104. @param {Function} completion generation function,
  105. invoked in the context of command instance.
  106. Accepts parameters:
  107. - {Object} opts completion options
  108. It can return promise or any other value treated as result.
  109. @returns {COA.Cmd} this instance (for chainability)
  110. ###
  111. comp: (@_comp) -> @
  112. ###*
  113. Apply function with arguments in context of command instance.
  114. @param {Function} fn
  115. @param {Array} args
  116. @returns {COA.Cmd} this instance (for chainability)
  117. ###
  118. apply: (fn, args...) ->
  119. fn.apply this, args
  120. @
  121. ###*
  122. Make command "helpful", i.e. add -h --help flags for print usage.
  123. @returns {COA.Cmd} this instance (for chainability)
  124. ###
  125. helpful: ->
  126. @opt()
  127. .name('help').title('Help')
  128. .short('h').long('help')
  129. .flag()
  130. .only()
  131. .act ->
  132. return @usage()
  133. .end()
  134. ###*
  135. Adds shell completion to command, adds "completion" subcommand,
  136. that makes all the magic.
  137. Must be called only on root command.
  138. @returns {COA.Cmd} this instance (for chainability)
  139. ###
  140. completable: ->
  141. @cmd()
  142. .name('completion')
  143. .apply(require './completion')
  144. .end()
  145. ###*
  146. Allow command to be extendable by external node.js modules.
  147. @param {String} [pattern] Pattern of node.js module to find subcommands at.
  148. @returns {COA.Cmd} this instance (for chainability)
  149. ###
  150. extendable: (pattern) ->
  151. @_ext = pattern or true
  152. @
  153. _exit: (msg, code) ->
  154. process.once 'exit', ->
  155. if msg then console.error msg
  156. process.exit code or 0
  157. ###*
  158. Build full usage text for current command instance.
  159. @returns {String} usage text
  160. ###
  161. usage: ->
  162. res = []
  163. if @_title then res.push @_fullTitle()
  164. res.push('', 'Usage:')
  165. if @_cmds.length then res.push(['', '',
  166. Color('lred', @_fullName()),
  167. Color('lblue', 'COMMAND'),
  168. Color('lgreen', '[OPTIONS]'),
  169. Color('lpurple', '[ARGS]')].join ' ')
  170. if @_opts.length + @_args.length then res.push(['', '',
  171. Color('lred', @_fullName()),
  172. Color('lgreen', '[OPTIONS]'),
  173. Color('lpurple', '[ARGS]')].join ' ')
  174. res.push(
  175. @_usages(@_cmds, 'Commands'),
  176. @_usages(@_opts, 'Options'),
  177. @_usages(@_args, 'Arguments'))
  178. res.join '\n'
  179. _usage: ->
  180. Color('lblue', @_name) + ' : ' + @_title
  181. _usages: (os, title) ->
  182. unless os.length then return
  183. res = ['', title + ':']
  184. for o in os
  185. res.push ' ' + o._usage()
  186. res.join '\n'
  187. _fullTitle: ->
  188. (if @_cmd is this then '' else @_cmd._fullTitle() + '\n') + @_title
  189. _fullName: ->
  190. (if this._cmd is this then '' else @_cmd._fullName() + ' ') + PATH.basename(@_name)
  191. _ejectOpt: (opts, opt) ->
  192. if (pos = opts.indexOf(opt)) >= 0
  193. if opts[pos]._arr
  194. opts[pos]
  195. else
  196. opts.splice(pos, 1)[0]
  197. _checkRequired: (opts, args) ->
  198. if not (@_opts.filter (o) -> o._only and o._name of opts).length
  199. all = @_opts.concat @_args
  200. while i = all.shift()
  201. if i._req and i._checkParsed opts, args
  202. return @reject i._requiredText()
  203. _parseCmd: (argv, unparsed = []) ->
  204. argv = argv.concat()
  205. optSeen = false
  206. while i = argv.shift()
  207. if not i.indexOf '-'
  208. optSeen = true
  209. if not optSeen and /^\w[\w-_]*$/.test(i)
  210. cmd = @_cmdsByName[i]
  211. if not cmd and @_ext
  212. # construct package name to require
  213. if typeof @_ext is 'string'
  214. if ~@_ext.indexOf('%s')
  215. # use formatted string
  216. pkg = UTIL.format(@_ext, i)
  217. else
  218. # just append subcommand name to the prefix
  219. pkg = @_ext + i
  220. else if @_ext is true
  221. # use default scheme: <command>-<subcommand>-<subcommand> and so on
  222. pkg = i
  223. c = @
  224. loop
  225. pkg = c._name + '-' + pkg
  226. if c._cmd is c then break
  227. c = c._cmd
  228. try
  229. cmdDesc = require(pkg)
  230. catch e
  231. if cmdDesc
  232. if typeof cmdDesc == 'function'
  233. # set create subcommand, set its name and apply imported function
  234. @cmd()
  235. .name(i)
  236. .apply(cmdDesc)
  237. .end()
  238. else if typeof cmdDesc == 'object'
  239. # register subcommand
  240. @cmd(cmdDesc)
  241. # set command name
  242. cmdDesc.name(i)
  243. else
  244. throw new Error 'Error: Unsupported command declaration type, ' +
  245. 'should be function or COA.Cmd() object'
  246. cmd = @_cmdsByName[i]
  247. if cmd
  248. return cmd._parseCmd argv, unparsed
  249. unparsed.push i
  250. { cmd: @, argv: unparsed }
  251. _parseOptsAndArgs: (argv) ->
  252. opts = {}
  253. args = {}
  254. nonParsedOpts = @_opts.concat()
  255. nonParsedArgs = @_args.concat()
  256. while i = argv.shift()
  257. # opt
  258. if i isnt '--' and not i.indexOf '-'
  259. if m = i.match /^(--\w[\w-_]*)=(.*)$/
  260. i = m[1]
  261. # suppress 'unknown argument' error for flag options with values
  262. if not @_optsByKey[i]._flag
  263. argv.unshift m[2]
  264. if opt = @_ejectOpt nonParsedOpts, @_optsByKey[i]
  265. if Q.isRejected(res = opt._parse argv, opts)
  266. return res
  267. else
  268. return @reject "Unknown option: #{ i }"
  269. # arg
  270. else
  271. if i is '--'
  272. i = argv.splice(0)
  273. i = if Array.isArray(i) then i else [i]
  274. while a = i.shift()
  275. if arg = nonParsedArgs.shift()
  276. if arg._arr then nonParsedArgs.unshift arg
  277. if Q.isRejected(res = arg._parse a, args)
  278. return res
  279. else
  280. return @reject "Unknown argument: #{ a }"
  281. # set defaults
  282. {
  283. opts: @_setDefaults(opts, nonParsedOpts),
  284. args: @_setDefaults(args, nonParsedArgs)
  285. }
  286. _setDefaults: (params, desc) ->
  287. for i in desc
  288. if i._name not of params and '_def' of i
  289. i._saveVal params, i._def
  290. params
  291. _processParams: (params, desc) ->
  292. notExists = []
  293. for i in desc
  294. n = i._name
  295. if n not of params
  296. notExists.push i
  297. continue
  298. vals = params[n]
  299. delete params[n]
  300. if not Array.isArray vals
  301. vals = [vals]
  302. for v in vals
  303. if Q.isRejected(res = i._saveVal(params, v))
  304. return res
  305. # set defaults
  306. @_setDefaults params, notExists
  307. _parseArr: (argv) ->
  308. Q.when @_parseCmd(argv), (p) ->
  309. Q.when p.cmd._parseOptsAndArgs(p.argv), (r) ->
  310. { cmd: p.cmd, opts: r.opts, args: r.args }
  311. _do: (input) ->
  312. Q.when input, (input) =>
  313. cmd = input.cmd
  314. [@_checkRequired].concat(cmd._act or []).reduce(
  315. (res, act) ->
  316. Q.when res, (res) ->
  317. act.call(
  318. cmd
  319. input.opts
  320. input.args
  321. res)
  322. undefined
  323. )
  324. ###*
  325. Parse arguments from simple format like NodeJS process.argv
  326. and run ahead current program, i.e. call process.exit when all actions done.
  327. @param {Array} argv
  328. @returns {COA.Cmd} this instance (for chainability)
  329. ###
  330. run: (argv = process.argv.slice(2)) ->
  331. cb = (code) => (res) =>
  332. if res
  333. @_exit res.stack ? res.toString(), res.exitCode ? code
  334. else
  335. @_exit()
  336. Q.when(@do(argv), cb(0), cb(1)).done()
  337. @
  338. ###*
  339. Convenient function to run command from tests.
  340. @param {Array} argv
  341. @returns {Q.Promise}
  342. ###
  343. do: (argv) ->
  344. @_do(@_parseArr argv || [])
  345. ###*
  346. Invoke specified (or current) command using provided
  347. options and arguments.
  348. @param {String|Array} cmds subcommand to invoke (optional)
  349. @param {Object} opts command options (optional)
  350. @param {Object} args command arguments (optional)
  351. @returns {Q.Promise}
  352. ###
  353. invoke: (cmds = [], opts = {}, args = {}) ->
  354. if typeof cmds == 'string'
  355. cmds = cmds.split(' ')
  356. if arguments.length < 3
  357. if not Array.isArray cmds
  358. args = opts
  359. opts = cmds
  360. cmds = []
  361. Q.when @_parseCmd(cmds), (p) =>
  362. if p.argv.length
  363. return @reject "Unknown command: " + cmds.join ' '
  364. Q.all([@_processParams(opts, @_opts), @_processParams(args, @_args)])
  365. .spread (opts, args) =>
  366. @_do({ cmd: p.cmd, opts: opts, args: args })
  367. # catch fails from .only() options
  368. .fail (res) =>
  369. if res and res.exitCode is 0
  370. res.toString()
  371. else
  372. @reject(res)
  373. ###*
  374. Return reject of actions results promise with error code.
  375. Use in .act() for return with error.
  376. @param {Object} reject reason
  377. You can customize toString() method and exitCode property
  378. of reason object.
  379. @returns {Q.promise} rejected promise
  380. ###
  381. reject: (reason) -> Q.reject(reason)
  382. ###*
  383. Finish chain for current subcommand and return parent command instance.
  384. @returns {COA.Cmd} parent command
  385. ###
  386. end: -> @_cmd