command.js 11 KB

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