| 'use strict' | |
| const inspect = require('util').inspect | |
| const isPromise = require('./is-promise') | |
| const { applyMiddleware, commandMiddlewareFactory } = require('./middleware') | |
| const path = require('path') | |
| const Parser = require('yargs-parser') | |
| const DEFAULT_MARKER = /(^\*)|(^\$0)/ | |
| // handles parsing positional arguments, | |
| // and populating argv with said positional | |
| // arguments. | |
| module.exports = function command (yargs, usage, validation, globalMiddleware) { | |
| const self = {} | |
| let handlers = {} | |
| let aliasMap = {} | |
| let defaultCommand | |
| globalMiddleware = globalMiddleware || [] | |
| self.addHandler = function addHandler (cmd, description, builder, handler, commandMiddleware) { | |
| let aliases = [] | |
| const middlewares = commandMiddlewareFactory(commandMiddleware) | |
| handler = handler || (() => {}) | |
| if (Array.isArray(cmd)) { | |
| aliases = cmd.slice(1) | |
| cmd = cmd[0] | |
| } else if (typeof cmd === 'object') { | |
| let command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd) | |
| if (cmd.aliases) command = [].concat(command).concat(cmd.aliases) | |
| self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler, cmd.middlewares) | |
| return | |
| } | |
| // allow a module to be provided instead of separate builder and handler | |
| if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') { | |
| self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares) | |
| return | |
| } | |
| // parse positionals out of cmd string | |
| const parsedCommand = self.parseCommand(cmd) | |
| // remove positional args from aliases only | |
| aliases = aliases.map(alias => self.parseCommand(alias).cmd) | |
| // check for default and filter out '*'' | |
| let isDefault = false | |
| const parsedAliases = [parsedCommand.cmd].concat(aliases).filter((c) => { | |
| if (DEFAULT_MARKER.test(c)) { | |
| isDefault = true | |
| return false | |
| } | |
| return true | |
| }) | |
| // standardize on $0 for default command. | |
| if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0') | |
| // shift cmd and aliases after filtering out '*' | |
| if (isDefault) { | |
| parsedCommand.cmd = parsedAliases[0] | |
| aliases = parsedAliases.slice(1) | |
| cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd) | |
| } | |
| // populate aliasMap | |
| aliases.forEach((alias) => { | |
| aliasMap[alias] = parsedCommand.cmd | |
| }) | |
| if (description !== false) { | |
| usage.command(cmd, description, isDefault, aliases) | |
| } | |
| handlers[parsedCommand.cmd] = { | |
| original: cmd, | |
| description: description, | |
| handler, | |
| builder: builder || {}, | |
| middlewares, | |
| demanded: parsedCommand.demanded, | |
| optional: parsedCommand.optional | |
| } | |
| if (isDefault) defaultCommand = handlers[parsedCommand.cmd] | |
| } | |
| self.addDirectory = function addDirectory (dir, context, req, callerFile, opts) { | |
| opts = opts || {} | |
| // disable recursion to support nested directories of subcommands | |
| if (typeof opts.recurse !== 'boolean') opts.recurse = false | |
| // exclude 'json', 'coffee' from require-directory defaults | |
| if (!Array.isArray(opts.extensions)) opts.extensions = ['js'] | |
| // allow consumer to define their own visitor function | |
| const parentVisit = typeof opts.visit === 'function' ? opts.visit : o => o | |
| // call addHandler via visitor function | |
| opts.visit = function visit (obj, joined, filename) { | |
| const visited = parentVisit(obj, joined, filename) | |
| // allow consumer to skip modules with their own visitor | |
| if (visited) { | |
| // check for cyclic reference | |
| // each command file path should only be seen once per execution | |
| if (~context.files.indexOf(joined)) return visited | |
| // keep track of visited files in context.files | |
| context.files.push(joined) | |
| self.addHandler(visited) | |
| } | |
| return visited | |
| } | |
| require('require-directory')({ require: req, filename: callerFile }, dir, opts) | |
| } | |
| // lookup module object from require()d command and derive name | |
| // if module was not require()d and no name given, throw error | |
| function moduleName (obj) { | |
| const mod = require('which-module')(obj) | |
| if (!mod) throw new Error(`No command name given for module: ${inspect(obj)}`) | |
| return commandFromFilename(mod.filename) | |
| } | |
| // derive command name from filename | |
| function commandFromFilename (filename) { | |
| return path.basename(filename, path.extname(filename)) | |
| } | |
| function extractDesc (obj) { | |
| for (let keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) { | |
| test = obj[keys[i]] | |
| if (typeof test === 'string' || typeof test === 'boolean') return test | |
| } | |
| return false | |
| } | |
| self.parseCommand = function parseCommand (cmd) { | |
| const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ') | |
| const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/) | |
| const bregex = /\.*[\][<>]/g | |
| const parsedCommand = { | |
| cmd: (splitCommand.shift()).replace(bregex, ''), | |
| demanded: [], | |
| optional: [] | |
| } | |
| splitCommand.forEach((cmd, i) => { | |
| let variadic = false | |
| cmd = cmd.replace(/\s/g, '') | |
| if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true | |
| if (/^\[/.test(cmd)) { | |
| parsedCommand.optional.push({ | |
| cmd: cmd.replace(bregex, '').split('|'), | |
| variadic | |
| }) | |
| } else { | |
| parsedCommand.demanded.push({ | |
| cmd: cmd.replace(bregex, '').split('|'), | |
| variadic | |
| }) | |
| } | |
| }) | |
| return parsedCommand | |
| } | |
| self.getCommands = () => Object.keys(handlers).concat(Object.keys(aliasMap)) | |
| self.getCommandHandlers = () => handlers | |
| self.hasDefaultCommand = () => !!defaultCommand | |
| self.runCommand = function runCommand (command, yargs, parsed, commandIndex) { | |
| let aliases = parsed.aliases | |
| const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand | |
| const currentContext = yargs.getContext() | |
| let numFiles = currentContext.files.length | |
| const parentCommands = currentContext.commands.slice() | |
| // what does yargs look like after the builder is run? | |
| let innerArgv = parsed.argv | |
| let innerYargs = null | |
| let positionalMap = {} | |
| if (command) { | |
| currentContext.commands.push(command) | |
| currentContext.fullCommands.push(commandHandler.original) | |
| } | |
| if (typeof commandHandler.builder === 'function') { | |
| // a function can be provided, which builds | |
| // up a yargs chain and possibly returns it. | |
| innerYargs = commandHandler.builder(yargs.reset(parsed.aliases)) | |
| if (!innerYargs || (typeof innerYargs._parseArgs !== 'function')) { | |
| innerYargs = yargs | |
| } | |
| if (shouldUpdateUsage(innerYargs)) { | |
| innerYargs.getUsageInstance().usage( | |
| usageFromParentCommandsCommandHandler(parentCommands, commandHandler), | |
| commandHandler.description | |
| ) | |
| } | |
| innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) | |
| aliases = innerYargs.parsed.aliases | |
| } else if (typeof commandHandler.builder === 'object') { | |
| // as a short hand, an object can instead be provided, specifying | |
| // the options that a command takes. | |
| innerYargs = yargs.reset(parsed.aliases) | |
| if (shouldUpdateUsage(innerYargs)) { | |
| innerYargs.getUsageInstance().usage( | |
| usageFromParentCommandsCommandHandler(parentCommands, commandHandler), | |
| commandHandler.description | |
| ) | |
| } | |
| Object.keys(commandHandler.builder).forEach((key) => { | |
| innerYargs.option(key, commandHandler.builder[key]) | |
| }) | |
| innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) | |
| aliases = innerYargs.parsed.aliases | |
| } | |
| if (!yargs._hasOutput()) { | |
| positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs) | |
| } | |
| const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares) | |
| applyMiddleware(innerArgv, yargs, middlewares, true) | |
| // we apply validation post-hoc, so that custom | |
| // checks get passed populated positional arguments. | |
| if (!yargs._hasOutput()) yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error) | |
| if (commandHandler.handler && !yargs._hasOutput()) { | |
| yargs._setHasOutput() | |
| // to simplify the parsing of positionals in commands, | |
| // we temporarily populate '--' rather than _, with arguments | |
| const populateDoubleDash = !!yargs.getOptions().configuration['populate--'] | |
| if (!populateDoubleDash) yargs._copyDoubleDash(innerArgv) | |
| innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false) | |
| let handlerResult | |
| if (isPromise(innerArgv)) { | |
| handlerResult = innerArgv.then(argv => commandHandler.handler(argv)) | |
| } else { | |
| handlerResult = commandHandler.handler(innerArgv) | |
| } | |
| if (isPromise(handlerResult)) { | |
| yargs.getUsageInstance().cacheHelpMessage() | |
| handlerResult.catch(error => { | |
| try { | |
| yargs.getUsageInstance().fail(null, error) | |
| } catch (err) { | |
| // fail's throwing would cause an unhandled rejection. | |
| } | |
| }) | |
| } | |
| } | |
| if (command) { | |
| currentContext.commands.pop() | |
| currentContext.fullCommands.pop() | |
| } | |
| numFiles = currentContext.files.length - numFiles | |
| if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles) | |
| return innerArgv | |
| } | |
| function shouldUpdateUsage (yargs) { | |
| return !yargs.getUsageInstance().getUsageDisabled() && | |
| yargs.getUsageInstance().getUsage().length === 0 | |
| } | |
| function usageFromParentCommandsCommandHandler (parentCommands, commandHandler) { | |
| const c = DEFAULT_MARKER.test(commandHandler.original) ? commandHandler.original.replace(DEFAULT_MARKER, '').trim() : commandHandler.original | |
| const pc = parentCommands.filter((c) => { return !DEFAULT_MARKER.test(c) }) | |
| pc.push(c) | |
| return `$0 ${pc.join(' ')}` | |
| } | |
| self.runDefaultBuilderOn = function (yargs) { | |
| if (shouldUpdateUsage(yargs)) { | |
| // build the root-level command string from the default string. | |
| const commandString = DEFAULT_MARKER.test(defaultCommand.original) | |
| ? defaultCommand.original : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ') | |
| yargs.getUsageInstance().usage( | |
| commandString, | |
| defaultCommand.description | |
| ) | |
| } | |
| const builder = defaultCommand.builder | |
| if (typeof builder === 'function') { | |
| builder(yargs) | |
| } else { | |
| Object.keys(builder).forEach((key) => { | |
| yargs.option(key, builder[key]) | |
| }) | |
| } | |
| } | |
| // transcribe all positional arguments "command <foo> <bar> [apple]" | |
| // onto argv. | |
| function populatePositionals (commandHandler, argv, context, yargs) { | |
| argv._ = argv._.slice(context.commands.length) // nuke the current commands | |
| const demanded = commandHandler.demanded.slice(0) | |
| const optional = commandHandler.optional.slice(0) | |
| const positionalMap = {} | |
| validation.positionalCount(demanded.length, argv._.length) | |
| while (demanded.length) { | |
| const demand = demanded.shift() | |
| populatePositional(demand, argv, positionalMap) | |
| } | |
| while (optional.length) { | |
| const maybe = optional.shift() | |
| populatePositional(maybe, argv, positionalMap) | |
| } | |
| argv._ = context.commands.concat(argv._) | |
| postProcessPositionals(argv, positionalMap, self.cmdToParseOptions(commandHandler.original)) | |
| return positionalMap | |
| } | |
| function populatePositional (positional, argv, positionalMap, parseOptions) { | |
| const cmd = positional.cmd[0] | |
| if (positional.variadic) { | |
| positionalMap[cmd] = argv._.splice(0).map(String) | |
| } else { | |
| if (argv._.length) positionalMap[cmd] = [String(argv._.shift())] | |
| } | |
| } | |
| // we run yargs-parser against the positional arguments | |
| // applying the same parsing logic used for flags. | |
| function postProcessPositionals (argv, positionalMap, parseOptions) { | |
| // combine the parsing hints we've inferred from the command | |
| // string with explicitly configured parsing hints. | |
| const options = Object.assign({}, yargs.getOptions()) | |
| options.default = Object.assign(parseOptions.default, options.default) | |
| options.alias = Object.assign(parseOptions.alias, options.alias) | |
| options.array = options.array.concat(parseOptions.array) | |
| delete options.config // don't load config when processing positionals. | |
| const unparsed = [] | |
| Object.keys(positionalMap).forEach((key) => { | |
| positionalMap[key].map((value) => { | |
| unparsed.push(`--${key}`) | |
| unparsed.push(value) | |
| }) | |
| }) | |
| // short-circuit parse. | |
| if (!unparsed.length) return | |
| const config = Object.assign({}, options.configuration, { | |
| 'populate--': true | |
| }) | |
| const parsed = Parser.detailed(unparsed, Object.assign({}, options, { | |
| configuration: config | |
| })) | |
| if (parsed.error) { | |
| yargs.getUsageInstance().fail(parsed.error.message, parsed.error) | |
| } else { | |
| // only copy over positional keys (don't overwrite | |
| // flag arguments that were already parsed). | |
| const positionalKeys = Object.keys(positionalMap) | |
| Object.keys(positionalMap).forEach((key) => { | |
| [].push.apply(positionalKeys, parsed.aliases[key]) | |
| }) | |
| Object.keys(parsed.argv).forEach((key) => { | |
| if (positionalKeys.indexOf(key) !== -1) { | |
| // any new aliases need to be placed in positionalMap, which | |
| // is used for validation. | |
| if (!positionalMap[key]) positionalMap[key] = parsed.argv[key] | |
| argv[key] = parsed.argv[key] | |
| } | |
| }) | |
| } | |
| } | |
| self.cmdToParseOptions = function (cmdString) { | |
| const parseOptions = { | |
| array: [], | |
| default: {}, | |
| alias: {}, | |
| demand: {} | |
| } | |
| const parsed = self.parseCommand(cmdString) | |
| parsed.demanded.forEach((d) => { | |
| const cmds = d.cmd.slice(0) | |
| const cmd = cmds.shift() | |
| if (d.variadic) { | |
| parseOptions.array.push(cmd) | |
| parseOptions.default[cmd] = [] | |
| } | |
| cmds.forEach((c) => { | |
| parseOptions.alias[cmd] = c | |
| }) | |
| parseOptions.demand[cmd] = true | |
| }) | |
| parsed.optional.forEach((o) => { | |
| const cmds = o.cmd.slice(0) | |
| const cmd = cmds.shift() | |
| if (o.variadic) { | |
| parseOptions.array.push(cmd) | |
| parseOptions.default[cmd] = [] | |
| } | |
| cmds.forEach((c) => { | |
| parseOptions.alias[cmd] = c | |
| }) | |
| }) | |
| return parseOptions | |
| } | |
| self.reset = () => { | |
| handlers = {} | |
| aliasMap = {} | |
| defaultCommand = undefined | |
| return self | |
| } | |
| // used by yargs.parse() to freeze | |
| // the state of commands such that | |
| // we can apply .parse() multiple times | |
| // with the same yargs instance. | |
| let frozens = [] | |
| self.freeze = () => { | |
| let frozen = {} | |
| frozens.push(frozen) | |
| frozen.handlers = handlers | |
| frozen.aliasMap = aliasMap | |
| frozen.defaultCommand = defaultCommand | |
| } | |
| self.unfreeze = () => { | |
| let frozen = frozens.pop() | |
| handlers = frozen.handlers | |
| aliasMap = frozen.aliasMap | |
| defaultCommand = frozen.defaultCommand | |
| } | |
| return self | |
| } |