1 const path = require('path')
2 const inspect = require('util').inspect
3 const requireDirectory = require('require-directory')
4 const whichModule = require('which-module')
6 // handles parsing positional arguments,
7 // and populating argv with said positional
9 module.exports = function (yargs, usage, validation) {
13 self.addHandler = function (cmd, description, builder, handler) {
14 if (typeof cmd === 'object') {
15 const commandString = typeof cmd.command === 'string' ? cmd.command : moduleName(cmd)
16 self.addHandler(commandString, extractDesc(cmd), cmd.builder, cmd.handler)
20 // allow a module to be provided instead of separate builder and handler
21 if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
22 self.addHandler(cmd, description, builder.builder, builder.handler)
26 if (description !== false) {
27 usage.command(cmd, description)
30 // we should not register a handler if no
31 // builder is provided, e.g., user will
32 // handle command themselves with '_'.
33 var parsedCommand = parseCommand(cmd)
34 handlers[parsedCommand.cmd] = {
37 // TODO: default to a noop builder in
40 demanded: parsedCommand.demanded,
41 optional: parsedCommand.optional
45 self.addDirectory = function (dir, context, req, callerFile, opts) {
47 // disable recursion to support nested directories of subcommands
48 if (typeof opts.recurse !== 'boolean') opts.recurse = false
49 // exclude 'json', 'coffee' from require-directory defaults
50 if (!Array.isArray(opts.extensions)) opts.extensions = ['js']
51 // allow consumer to define their own visitor function
52 const parentVisit = typeof opts.visit === 'function' ? opts.visit : function (o) { return o }
53 // call addHandler via visitor function
54 opts.visit = function (obj, joined, filename) {
55 const visited = parentVisit(obj, joined, filename)
56 // allow consumer to skip modules with their own visitor
58 // check for cyclic reference
59 // each command file path should only be seen once per execution
60 if (~context.files.indexOf(joined)) return visited
61 // keep track of visited files in context.files
62 context.files.push(joined)
63 self.addHandler(visited)
67 requireDirectory({ require: req, filename: callerFile }, dir, opts)
70 // lookup module object from require()d command and derive name
71 // if module was not require()d and no name given, throw error
72 function moduleName (obj) {
73 const mod = whichModule(obj)
74 if (!mod) throw new Error('No command name given for module: ' + inspect(obj))
75 return commandFromFilename(mod.filename)
78 // derive command name from filename
79 function commandFromFilename (filename) {
80 return path.basename(filename, path.extname(filename))
83 function extractDesc (obj) {
84 for (var keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) {
86 if (typeof test === 'string' || typeof test === 'boolean') return test
91 function parseCommand (cmd) {
92 var splitCommand = cmd.split(/\s/)
93 var bregex = /\.*[\][<>]/g
95 cmd: (splitCommand.shift()).replace(bregex, ''),
99 splitCommand.forEach(function (cmd, i) {
101 if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true
102 if (/^\[/.test(cmd)) {
103 parsedCommand.optional.push({
104 cmd: cmd.replace(bregex, ''),
108 parsedCommand.demanded.push({
109 cmd: cmd.replace(bregex, ''),
117 self.getCommands = function () {
118 return Object.keys(handlers)
121 self.getCommandHandlers = function () {
125 self.runCommand = function (command, yargs, parsed) {
126 var argv = parsed.argv
127 var commandHandler = handlers[command]
129 var currentContext = yargs.getContext()
130 var parentCommands = currentContext.commands.slice()
131 currentContext.commands.push(command)
132 if (commandHandler.builder && typeof commandHandler.builder === 'function') {
133 // a function can be provided, which interacts which builds
134 // up a yargs chain and returns it.
135 innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
136 // if the builder function did not yet parse argv with reset yargs
137 // and did not explicitly set a usage() string, then apply the
138 // original command string as usage() for consistent behavior with
139 // options object below
140 if (yargs.parsed === false && typeof yargs.getUsageInstance().getUsage() === 'undefined') {
141 yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
143 innerArgv = innerArgv ? innerArgv.argv : argv
144 } else if (commandHandler.builder && typeof commandHandler.builder === 'object') {
145 // as a short hand, an object can instead be provided, specifying
146 // the options that a command takes.
147 innerArgv = yargs.reset(parsed.aliases)
148 innerArgv.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
149 Object.keys(commandHandler.builder).forEach(function (key) {
150 innerArgv.option(key, commandHandler.builder[key])
152 innerArgv = innerArgv.argv
155 populatePositional(commandHandler, innerArgv, currentContext)
157 if (commandHandler.handler) {
158 commandHandler.handler(innerArgv)
160 currentContext.commands.pop()
164 function populatePositional (commandHandler, argv, context) {
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)
169 validation.positionalCount(demanded.length, argv._.length)
171 while (demanded.length) {
172 var demand = demanded.shift()
173 if (demand.variadic) argv[demand.cmd] = []
174 if (!argv._.length) break
175 if (demand.variadic) argv[demand.cmd] = argv._.splice(0)
176 else argv[demand.cmd] = argv._.shift()
179 while (optional.length) {
180 var maybe = optional.shift()
181 if (maybe.variadic) argv[maybe.cmd] = []
182 if (!argv._.length) break
183 if (maybe.variadic) argv[maybe.cmd] = argv._.splice(0)
184 else argv[maybe.cmd] = argv._.shift()
187 argv._ = context.commands.concat(argv._)
190 self.reset = function () {