import { assertNotStrictEqual, } from './typings/common-types.js'; import { isPromise } from './utils/is-promise.js'; import { applyMiddleware, commandMiddlewareFactory, } from './middleware.js'; import { parseCommand } from './parse-command.js'; import { isYargsInstance, } from './yargs-factory.js'; import whichModule from './utils/which-module.js'; const DEFAULT_MARKER = /(^\*)|(^\$0)/; export function command(yargs, usage, validation, globalMiddleware = [], shim) { const self = {}; let handlers = {}; let aliasMap = {}; let defaultCommand; self.addHandler = function addHandler(cmd, description, builder, handler, commandMiddleware, deprecated) { let aliases = []; const middlewares = commandMiddlewareFactory(commandMiddleware); handler = handler || (() => { }); if (Array.isArray(cmd)) { if (isCommandAndAliases(cmd)) { [cmd, ...aliases] = cmd; } else { for (const command of cmd) { self.addHandler(command); } } } else if (isCommandHandlerDefinition(cmd)) { 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, cmd.deprecated); return; } else if (isCommandBuilderDefinition(builder)) { self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares, builder.deprecated); return; } if (typeof cmd === 'string') { const parsedCommand = parseCommand(cmd); aliases = aliases.map(alias => parseCommand(alias).cmd); let isDefault = false; const parsedAliases = [parsedCommand.cmd].concat(aliases).filter(c => { if (DEFAULT_MARKER.test(c)) { isDefault = true; return false; } return true; }); if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0'); if (isDefault) { parsedCommand.cmd = parsedAliases[0]; aliases = parsedAliases.slice(1); cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd); } aliases.forEach(alias => { aliasMap[alias] = parsedCommand.cmd; }); if (description !== false) { usage.command(cmd, description, isDefault, aliases, deprecated); } handlers[parsedCommand.cmd] = { original: cmd, description, handler, builder: builder || {}, middlewares, deprecated, demanded: parsedCommand.demanded, optional: parsedCommand.optional, }; if (isDefault) defaultCommand = handlers[parsedCommand.cmd]; } }; self.addDirectory = function addDirectory(dir, context, req, callerFile, opts) { opts = opts || {}; if (typeof opts.recurse !== 'boolean') opts.recurse = false; if (!Array.isArray(opts.extensions)) opts.extensions = ['js']; const parentVisit = typeof opts.visit === 'function' ? opts.visit : (o) => o; opts.visit = function visit(obj, joined, filename) { const visited = parentVisit(obj, joined, filename); if (visited) { if (~context.files.indexOf(joined)) return visited; context.files.push(joined); self.addHandler(visited); } return visited; }; shim.requireDirectory({ require: req, filename: callerFile }, dir, opts); }; function moduleName(obj) { const mod = whichModule(obj); if (!mod) throw new Error(`No command name given for module: ${shim.inspect(obj)}`); return commandFromFilename(mod.filename); } function commandFromFilename(filename) { return shim.path.basename(filename, shim.path.extname(filename)); } function extractDesc({ describe, description, desc, }) { for (const test of [describe, description, desc]) { if (typeof test === 'string' || test === false) return test; assertNotStrictEqual(test, true, shim); } return false; } 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(); let innerArgv = parsed.argv; let positionalMap = {}; if (command) { currentContext.commands.push(command); currentContext.fullCommands.push(commandHandler.original); } const builder = commandHandler.builder; if (isCommandBuilderCallback(builder)) { const builderOutput = builder(yargs.reset(parsed.aliases)); const innerYargs = isYargsInstance(builderOutput) ? builderOutput : 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 (isCommandBuilderOptionDefinitions(builder)) { const 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, builder[key]); }); innerArgv = innerYargs._parseArgs(null, null, true, commandIndex); aliases = innerYargs.parsed.aliases; } if (!yargs._hasOutput()) { positionalMap = populatePositionals(commandHandler, innerArgv, currentContext); } const middlewares = globalMiddleware .slice(0) .concat(commandHandler.middlewares); applyMiddleware(innerArgv, yargs, middlewares, true); if (!yargs._hasOutput()) { yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error, !command); } if (commandHandler.handler && !yargs._hasOutput()) { yargs._setHasOutput(); const populateDoubleDash = !!yargs.getOptions().configuration['populate--']; yargs._postProcess(innerArgv, populateDoubleDash); innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false); let handlerResult; if (isPromise(innerArgv)) { handlerResult = innerArgv.then(argv => commandHandler.handler(argv)); } else { handlerResult = commandHandler.handler(innerArgv); } const handlerFinishCommand = yargs.getHandlerFinishCommand(); if (isPromise(handlerResult)) { yargs.getUsageInstance().cacheHelpMessage(); handlerResult .then(value => { if (handlerFinishCommand) { handlerFinishCommand(value); } }) .catch(error => { try { yargs.getUsageInstance().fail(null, error); } catch (err) { } }) .then(() => { yargs.getUsageInstance().clearCachedHelpMessage(); }); } else { if (handlerFinishCommand) { handlerFinishCommand(handlerResult); } } } 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) { assertNotStrictEqual(defaultCommand, undefined, shim); if (shouldUpdateUsage(yargs)) { const commandString = DEFAULT_MARKER.test(defaultCommand.original) ? defaultCommand.original : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 '); yargs.getUsageInstance().usage(commandString, defaultCommand.description); } const builder = defaultCommand.builder; if (isCommandBuilderCallback(builder)) { builder(yargs); } else if (!isCommandBuilderDefinition(builder)) { Object.keys(builder).forEach(key => { yargs.option(key, builder[key]); }); } }; function populatePositionals(commandHandler, argv, context) { argv._ = argv._.slice(context.commands.length); 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._.map(a => '' + a)); postProcessPositionals(argv, positionalMap, self.cmdToParseOptions(commandHandler.original)); return positionalMap; } function populatePositional(positional, argv, positionalMap) { const cmd = positional.cmd[0]; if (positional.variadic) { positionalMap[cmd] = argv._.splice(0).map(String); } else { if (argv._.length) positionalMap[cmd] = [String(argv._.shift())]; } } function postProcessPositionals(argv, positionalMap, parseOptions) { const options = Object.assign({}, yargs.getOptions()); options.default = Object.assign(parseOptions.default, options.default); for (const key of Object.keys(parseOptions.alias)) { options.alias[key] = (options.alias[key] || []).concat(parseOptions.alias[key]); } options.array = options.array.concat(parseOptions.array); options.config = {}; const unparsed = []; Object.keys(positionalMap).forEach(key => { positionalMap[key].map(value => { if (options.configuration['unknown-options-as-args']) options.key[key] = true; unparsed.push(`--${key}`); unparsed.push(value); }); }); if (!unparsed.length) return; const config = Object.assign({}, options.configuration, { 'populate--': true, }); const parsed = shim.Parser.detailed(unparsed, Object.assign({}, options, { configuration: config, })); if (parsed.error) { yargs.getUsageInstance().fail(parsed.error.message, parsed.error); } else { const positionalKeys = Object.keys(positionalMap); Object.keys(positionalMap).forEach(key => { positionalKeys.push(...parsed.aliases[key]); }); Object.keys(parsed.argv).forEach(key => { if (positionalKeys.indexOf(key) !== -1) { 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 = parseCommand(cmdString); parsed.demanded.forEach(d => { const [cmd, ...aliases] = d.cmd; if (d.variadic) { parseOptions.array.push(cmd); parseOptions.default[cmd] = []; } parseOptions.alias[cmd] = aliases; parseOptions.demand[cmd] = true; }); parsed.optional.forEach(o => { const [cmd, ...aliases] = o.cmd; if (o.variadic) { parseOptions.array.push(cmd); parseOptions.default[cmd] = []; } parseOptions.alias[cmd] = aliases; }); return parseOptions; }; self.reset = () => { handlers = {}; aliasMap = {}; defaultCommand = undefined; return self; }; const frozens = []; self.freeze = () => { frozens.push({ handlers, aliasMap, defaultCommand, }); }; self.unfreeze = () => { const frozen = frozens.pop(); assertNotStrictEqual(frozen, undefined, shim); ({ handlers, aliasMap, defaultCommand } = frozen); }; return self; } export function isCommandBuilderDefinition(builder) { return (typeof builder === 'object' && !!builder.builder && typeof builder.handler === 'function'); } function isCommandAndAliases(cmd) { if (cmd.every(c => typeof c === 'string')) { return true; } else { return false; } } export function isCommandBuilderCallback(builder) { return typeof builder === 'function'; } function isCommandBuilderOptionDefinitions(builder) { return typeof builder === 'object'; } export function isCommandHandlerDefinition(cmd) { return typeof cmd === 'object' && !Array.isArray(cmd); }