command.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. const path = require('path')
  2. const inspect = require('util').inspect
  3. const camelCase = require('camelcase')
  4. // handles parsing positional arguments,
  5. // and populating argv with said positional
  6. // arguments.
  7. module.exports = function (yargs, usage, validation) {
  8. const self = {}
  9. var handlers = {}
  10. var aliasMap = {}
  11. self.addHandler = function (cmd, description, builder, handler) {
  12. var aliases = []
  13. if (Array.isArray(cmd)) {
  14. aliases = cmd.slice(1)
  15. cmd = cmd[0]
  16. } else if (typeof cmd === 'object') {
  17. var command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd)
  18. if (cmd.aliases) command = [].concat(command).concat(cmd.aliases)
  19. self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler)
  20. return
  21. }
  22. // allow a module to be provided instead of separate builder and handler
  23. if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
  24. self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler)
  25. return
  26. }
  27. var parsedCommand = parseCommand(cmd)
  28. aliases = aliases.map(function (alias) {
  29. alias = parseCommand(alias).cmd // remove positional args
  30. aliasMap[alias] = parsedCommand.cmd
  31. return alias
  32. })
  33. if (description !== false) {
  34. usage.command(cmd, description, aliases)
  35. }
  36. handlers[parsedCommand.cmd] = {
  37. original: cmd,
  38. handler: handler,
  39. builder: builder || {},
  40. demanded: parsedCommand.demanded,
  41. optional: parsedCommand.optional
  42. }
  43. }
  44. self.addDirectory = function (dir, context, req, callerFile, opts) {
  45. opts = opts || {}
  46. // disable recursion to support nested directories of subcommands
  47. if (typeof opts.recurse !== 'boolean') opts.recurse = false
  48. // exclude 'json', 'coffee' from require-directory defaults
  49. if (!Array.isArray(opts.extensions)) opts.extensions = ['js']
  50. // allow consumer to define their own visitor function
  51. const parentVisit = typeof opts.visit === 'function' ? opts.visit : function (o) { return o }
  52. // call addHandler via visitor function
  53. opts.visit = function (obj, joined, filename) {
  54. const visited = parentVisit(obj, joined, filename)
  55. // allow consumer to skip modules with their own visitor
  56. if (visited) {
  57. // check for cyclic reference
  58. // each command file path should only be seen once per execution
  59. if (~context.files.indexOf(joined)) return visited
  60. // keep track of visited files in context.files
  61. context.files.push(joined)
  62. self.addHandler(visited)
  63. }
  64. return visited
  65. }
  66. require('require-directory')({ require: req, filename: callerFile }, dir, opts)
  67. }
  68. // lookup module object from require()d command and derive name
  69. // if module was not require()d and no name given, throw error
  70. function moduleName (obj) {
  71. const mod = require('which-module')(obj)
  72. if (!mod) throw new Error('No command name given for module: ' + inspect(obj))
  73. return commandFromFilename(mod.filename)
  74. }
  75. // derive command name from filename
  76. function commandFromFilename (filename) {
  77. return path.basename(filename, path.extname(filename))
  78. }
  79. function extractDesc (obj) {
  80. for (var keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) {
  81. test = obj[keys[i]]
  82. if (typeof test === 'string' || typeof test === 'boolean') return test
  83. }
  84. return false
  85. }
  86. function parseCommand (cmd) {
  87. var extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ')
  88. var splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/)
  89. var bregex = /\.*[\][<>]/g
  90. var parsedCommand = {
  91. cmd: (splitCommand.shift()).replace(bregex, ''),
  92. demanded: [],
  93. optional: []
  94. }
  95. splitCommand.forEach(function (cmd, i) {
  96. var variadic = false
  97. cmd = cmd.replace(/\s/g, '')
  98. if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true
  99. if (/^\[/.test(cmd)) {
  100. parsedCommand.optional.push({
  101. cmd: cmd.replace(bregex, '').split('|'),
  102. variadic: variadic
  103. })
  104. } else {
  105. parsedCommand.demanded.push({
  106. cmd: cmd.replace(bregex, '').split('|'),
  107. variadic: variadic
  108. })
  109. }
  110. })
  111. return parsedCommand
  112. }
  113. self.getCommands = function () {
  114. return Object.keys(handlers).concat(Object.keys(aliasMap))
  115. }
  116. self.getCommandHandlers = function () {
  117. return handlers
  118. }
  119. self.runCommand = function (command, yargs, parsed) {
  120. var argv = parsed.argv
  121. var commandHandler = handlers[command] || handlers[aliasMap[command]]
  122. var innerArgv = argv
  123. var currentContext = yargs.getContext()
  124. var numFiles = currentContext.files.length
  125. var parentCommands = currentContext.commands.slice()
  126. currentContext.commands.push(command)
  127. if (typeof commandHandler.builder === 'function') {
  128. // a function can be provided, which builds
  129. // up a yargs chain and possibly returns it.
  130. innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
  131. // if the builder function did not yet parse argv with reset yargs
  132. // and did not explicitly set a usage() string, then apply the
  133. // original command string as usage() for consistent behavior with
  134. // options object below
  135. if (yargs.parsed === false) {
  136. if (typeof yargs.getUsageInstance().getUsage() === 'undefined') {
  137. yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
  138. }
  139. innerArgv = innerArgv ? innerArgv.argv : yargs.argv
  140. } else {
  141. innerArgv = yargs.parsed.argv
  142. }
  143. } else if (typeof commandHandler.builder === 'object') {
  144. // as a short hand, an object can instead be provided, specifying
  145. // the options that a command takes.
  146. innerArgv = yargs.reset(parsed.aliases)
  147. innerArgv.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
  148. Object.keys(commandHandler.builder).forEach(function (key) {
  149. innerArgv.option(key, commandHandler.builder[key])
  150. })
  151. innerArgv = innerArgv.argv
  152. }
  153. if (!yargs._hasOutput()) populatePositionals(commandHandler, innerArgv, currentContext, yargs)
  154. if (commandHandler.handler && !yargs._hasOutput()) {
  155. commandHandler.handler(innerArgv)
  156. }
  157. currentContext.commands.pop()
  158. numFiles = currentContext.files.length - numFiles
  159. if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles)
  160. return innerArgv
  161. }
  162. // transcribe all positional arguments "command <foo> <bar> [apple]"
  163. // onto argv.
  164. function populatePositionals (commandHandler, argv, context, yargs) {
  165. argv._ = argv._.slice(context.commands.length) // nuke the current commands
  166. var demanded = commandHandler.demanded.slice(0)
  167. var optional = commandHandler.optional.slice(0)
  168. validation.positionalCount(demanded.length, argv._.length)
  169. while (demanded.length) {
  170. var demand = demanded.shift()
  171. populatePositional(demand, argv, yargs)
  172. }
  173. while (optional.length) {
  174. var maybe = optional.shift()
  175. populatePositional(maybe, argv, yargs)
  176. }
  177. argv._ = context.commands.concat(argv._)
  178. }
  179. // populate a single positional argument and its
  180. // aliases onto argv.
  181. function populatePositional (positional, argv, yargs) {
  182. // "positional" consists of the positional.cmd, an array representing
  183. // the positional's name and aliases, and positional.variadic
  184. // indicating whether or not it is a variadic array.
  185. var variadics = null
  186. var value = null
  187. for (var i = 0, cmd; (cmd = positional.cmd[i]) !== undefined; i++) {
  188. if (positional.variadic) {
  189. if (variadics) argv[cmd] = variadics.slice(0)
  190. else argv[cmd] = variadics = argv._.splice(0)
  191. } else {
  192. if (!value && !argv._.length) continue
  193. if (value) argv[cmd] = value
  194. else argv[cmd] = value = argv._.shift()
  195. }
  196. postProcessPositional(yargs, argv, cmd)
  197. addCamelCaseExpansions(argv, cmd)
  198. }
  199. }
  200. // TODO move positional arg logic to yargs-parser and remove this duplication
  201. function postProcessPositional (yargs, argv, key) {
  202. var coerce = yargs.getOptions().coerce[key]
  203. if (typeof coerce === 'function') {
  204. try {
  205. argv[key] = coerce(argv[key])
  206. } catch (err) {
  207. yargs.getUsageInstance().fail(err.message, err)
  208. }
  209. }
  210. }
  211. function addCamelCaseExpansions (argv, option) {
  212. if (/-/.test(option)) {
  213. const cc = camelCase(option)
  214. if (typeof argv[option] === 'object') argv[cc] = argv[option].slice(0)
  215. else argv[cc] = argv[option]
  216. }
  217. }
  218. self.reset = function () {
  219. handlers = {}
  220. aliasMap = {}
  221. return self
  222. }
  223. // used by yargs.parse() to freeze
  224. // the state of commands such that
  225. // we can apply .parse() multiple times
  226. // with the same yargs instance.
  227. var frozen
  228. self.freeze = function () {
  229. frozen = {}
  230. frozen.handlers = handlers
  231. frozen.aliasMap = aliasMap
  232. }
  233. self.unfreeze = function () {
  234. handlers = frozen.handlers
  235. aliasMap = frozen.aliasMap
  236. frozen = undefined
  237. }
  238. return self
  239. }