command.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.isCommandBuilderCallback = exports.isCommandBuilderDefinition = exports.isCommandHandlerDefinition = exports.command = void 0;
  4. const common_types_1 = require("./common-types");
  5. const is_promise_1 = require("./is-promise");
  6. const middleware_1 = require("./middleware");
  7. const parse_command_1 = require("./parse-command");
  8. const path = require("path");
  9. const util_1 = require("util");
  10. const yargs_1 = require("./yargs");
  11. const requireDirectory = require("require-directory");
  12. const whichModule = require("which-module");
  13. const Parser = require("yargs-parser");
  14. const DEFAULT_MARKER = /(^\*)|(^\$0)/;
  15. // handles parsing positional arguments,
  16. // and populating argv with said positional
  17. // arguments.
  18. function command(yargs, usage, validation, globalMiddleware = []) {
  19. const self = {};
  20. let handlers = {};
  21. let aliasMap = {};
  22. let defaultCommand;
  23. self.addHandler = function addHandler(cmd, description, builder, handler, commandMiddleware, deprecated) {
  24. let aliases = [];
  25. const middlewares = middleware_1.commandMiddlewareFactory(commandMiddleware);
  26. handler = handler || (() => { });
  27. if (Array.isArray(cmd)) {
  28. aliases = cmd.slice(1);
  29. cmd = cmd[0];
  30. }
  31. else if (isCommandHandlerDefinition(cmd)) {
  32. let command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd);
  33. if (cmd.aliases)
  34. command = [].concat(command).concat(cmd.aliases);
  35. self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler, cmd.middlewares, cmd.deprecated);
  36. return;
  37. }
  38. // allow a module to be provided instead of separate builder and handler
  39. if (isCommandBuilderDefinition(builder)) {
  40. self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares, builder.deprecated);
  41. return;
  42. }
  43. // parse positionals out of cmd string
  44. const parsedCommand = parse_command_1.parseCommand(cmd);
  45. // remove positional args from aliases only
  46. aliases = aliases.map(alias => parse_command_1.parseCommand(alias).cmd);
  47. // check for default and filter out '*''
  48. let isDefault = false;
  49. const parsedAliases = [parsedCommand.cmd].concat(aliases).filter((c) => {
  50. if (DEFAULT_MARKER.test(c)) {
  51. isDefault = true;
  52. return false;
  53. }
  54. return true;
  55. });
  56. // standardize on $0 for default command.
  57. if (parsedAliases.length === 0 && isDefault)
  58. parsedAliases.push('$0');
  59. // shift cmd and aliases after filtering out '*'
  60. if (isDefault) {
  61. parsedCommand.cmd = parsedAliases[0];
  62. aliases = parsedAliases.slice(1);
  63. cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
  64. }
  65. // populate aliasMap
  66. aliases.forEach((alias) => {
  67. aliasMap[alias] = parsedCommand.cmd;
  68. });
  69. if (description !== false) {
  70. usage.command(cmd, description, isDefault, aliases, deprecated);
  71. }
  72. handlers[parsedCommand.cmd] = {
  73. original: cmd,
  74. description,
  75. handler,
  76. builder: builder || {},
  77. middlewares,
  78. deprecated,
  79. demanded: parsedCommand.demanded,
  80. optional: parsedCommand.optional
  81. };
  82. if (isDefault)
  83. defaultCommand = handlers[parsedCommand.cmd];
  84. };
  85. self.addDirectory = function addDirectory(dir, context, req, callerFile, opts) {
  86. opts = opts || {};
  87. // disable recursion to support nested directories of subcommands
  88. if (typeof opts.recurse !== 'boolean')
  89. opts.recurse = false;
  90. // exclude 'json', 'coffee' from require-directory defaults
  91. if (!Array.isArray(opts.extensions))
  92. opts.extensions = ['js'];
  93. // allow consumer to define their own visitor function
  94. const parentVisit = typeof opts.visit === 'function' ? opts.visit : (o) => o;
  95. // call addHandler via visitor function
  96. opts.visit = function visit(obj, joined, filename) {
  97. const visited = parentVisit(obj, joined, filename);
  98. // allow consumer to skip modules with their own visitor
  99. if (visited) {
  100. // check for cyclic reference
  101. // each command file path should only be seen once per execution
  102. if (~context.files.indexOf(joined))
  103. return visited;
  104. // keep track of visited files in context.files
  105. context.files.push(joined);
  106. self.addHandler(visited);
  107. }
  108. return visited;
  109. };
  110. requireDirectory({ require: req, filename: callerFile }, dir, opts);
  111. };
  112. // lookup module object from require()d command and derive name
  113. // if module was not require()d and no name given, throw error
  114. function moduleName(obj) {
  115. const mod = whichModule(obj);
  116. if (!mod)
  117. throw new Error(`No command name given for module: ${util_1.inspect(obj)}`);
  118. return commandFromFilename(mod.filename);
  119. }
  120. // derive command name from filename
  121. function commandFromFilename(filename) {
  122. return path.basename(filename, path.extname(filename));
  123. }
  124. function extractDesc({ describe, description, desc }) {
  125. for (const test of [describe, description, desc]) {
  126. if (typeof test === 'string' || test === false)
  127. return test;
  128. common_types_1.assertNotStrictEqual(test, true);
  129. }
  130. return false;
  131. }
  132. self.getCommands = () => Object.keys(handlers).concat(Object.keys(aliasMap));
  133. self.getCommandHandlers = () => handlers;
  134. self.hasDefaultCommand = () => !!defaultCommand;
  135. self.runCommand = function runCommand(command, yargs, parsed, commandIndex) {
  136. let aliases = parsed.aliases;
  137. const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand;
  138. const currentContext = yargs.getContext();
  139. let numFiles = currentContext.files.length;
  140. const parentCommands = currentContext.commands.slice();
  141. // what does yargs look like after the builder is run?
  142. let innerArgv = parsed.argv;
  143. let positionalMap = {};
  144. if (command) {
  145. currentContext.commands.push(command);
  146. currentContext.fullCommands.push(commandHandler.original);
  147. }
  148. const builder = commandHandler.builder;
  149. if (isCommandBuilderCallback(builder)) {
  150. // a function can be provided, which builds
  151. // up a yargs chain and possibly returns it.
  152. const builderOutput = builder(yargs.reset(parsed.aliases));
  153. const innerYargs = yargs_1.isYargsInstance(builderOutput) ? builderOutput : yargs;
  154. if (shouldUpdateUsage(innerYargs)) {
  155. innerYargs.getUsageInstance().usage(usageFromParentCommandsCommandHandler(parentCommands, commandHandler), commandHandler.description);
  156. }
  157. innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
  158. aliases = innerYargs.parsed.aliases;
  159. }
  160. else if (isCommandBuilderOptionDefinitions(builder)) {
  161. // as a short hand, an object can instead be provided, specifying
  162. // the options that a command takes.
  163. const innerYargs = yargs.reset(parsed.aliases);
  164. if (shouldUpdateUsage(innerYargs)) {
  165. innerYargs.getUsageInstance().usage(usageFromParentCommandsCommandHandler(parentCommands, commandHandler), commandHandler.description);
  166. }
  167. Object.keys(commandHandler.builder).forEach((key) => {
  168. innerYargs.option(key, builder[key]);
  169. });
  170. innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
  171. aliases = innerYargs.parsed.aliases;
  172. }
  173. if (!yargs._hasOutput()) {
  174. positionalMap = populatePositionals(commandHandler, innerArgv, currentContext);
  175. }
  176. const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares);
  177. middleware_1.applyMiddleware(innerArgv, yargs, middlewares, true);
  178. // we apply validation post-hoc, so that custom
  179. // checks get passed populated positional arguments.
  180. if (!yargs._hasOutput()) {
  181. yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error, !command);
  182. }
  183. if (commandHandler.handler && !yargs._hasOutput()) {
  184. yargs._setHasOutput();
  185. // to simplify the parsing of positionals in commands,
  186. // we temporarily populate '--' rather than _, with arguments
  187. const populateDoubleDash = !!yargs.getOptions().configuration['populate--'];
  188. if (!populateDoubleDash)
  189. yargs._copyDoubleDash(innerArgv);
  190. innerArgv = middleware_1.applyMiddleware(innerArgv, yargs, middlewares, false);
  191. let handlerResult;
  192. if (is_promise_1.isPromise(innerArgv)) {
  193. handlerResult = innerArgv.then(argv => commandHandler.handler(argv));
  194. }
  195. else {
  196. handlerResult = commandHandler.handler(innerArgv);
  197. }
  198. const handlerFinishCommand = yargs.getHandlerFinishCommand();
  199. if (is_promise_1.isPromise(handlerResult)) {
  200. yargs.getUsageInstance().cacheHelpMessage();
  201. handlerResult
  202. .then(value => {
  203. if (handlerFinishCommand) {
  204. handlerFinishCommand(value);
  205. }
  206. })
  207. .catch(error => {
  208. try {
  209. yargs.getUsageInstance().fail(null, error);
  210. }
  211. catch (err) {
  212. // fail's throwing would cause an unhandled rejection.
  213. }
  214. })
  215. .then(() => {
  216. yargs.getUsageInstance().clearCachedHelpMessage();
  217. });
  218. }
  219. else {
  220. if (handlerFinishCommand) {
  221. handlerFinishCommand(handlerResult);
  222. }
  223. }
  224. }
  225. if (command) {
  226. currentContext.commands.pop();
  227. currentContext.fullCommands.pop();
  228. }
  229. numFiles = currentContext.files.length - numFiles;
  230. if (numFiles > 0)
  231. currentContext.files.splice(numFiles * -1, numFiles);
  232. return innerArgv;
  233. };
  234. function shouldUpdateUsage(yargs) {
  235. return !yargs.getUsageInstance().getUsageDisabled() &&
  236. yargs.getUsageInstance().getUsage().length === 0;
  237. }
  238. function usageFromParentCommandsCommandHandler(parentCommands, commandHandler) {
  239. const c = DEFAULT_MARKER.test(commandHandler.original) ? commandHandler.original.replace(DEFAULT_MARKER, '').trim() : commandHandler.original;
  240. const pc = parentCommands.filter((c) => { return !DEFAULT_MARKER.test(c); });
  241. pc.push(c);
  242. return `$0 ${pc.join(' ')}`;
  243. }
  244. self.runDefaultBuilderOn = function (yargs) {
  245. common_types_1.assertNotStrictEqual(defaultCommand, undefined);
  246. if (shouldUpdateUsage(yargs)) {
  247. // build the root-level command string from the default string.
  248. const commandString = DEFAULT_MARKER.test(defaultCommand.original)
  249. ? defaultCommand.original : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ');
  250. yargs.getUsageInstance().usage(commandString, defaultCommand.description);
  251. }
  252. const builder = defaultCommand.builder;
  253. if (isCommandBuilderCallback(builder)) {
  254. builder(yargs);
  255. }
  256. else {
  257. Object.keys(builder).forEach((key) => {
  258. yargs.option(key, builder[key]);
  259. });
  260. }
  261. };
  262. // transcribe all positional arguments "command <foo> <bar> [apple]"
  263. // onto argv.
  264. function populatePositionals(commandHandler, argv, context) {
  265. argv._ = argv._.slice(context.commands.length); // nuke the current commands
  266. const demanded = commandHandler.demanded.slice(0);
  267. const optional = commandHandler.optional.slice(0);
  268. const positionalMap = {};
  269. validation.positionalCount(demanded.length, argv._.length);
  270. while (demanded.length) {
  271. const demand = demanded.shift();
  272. populatePositional(demand, argv, positionalMap);
  273. }
  274. while (optional.length) {
  275. const maybe = optional.shift();
  276. populatePositional(maybe, argv, positionalMap);
  277. }
  278. argv._ = context.commands.concat(argv._);
  279. postProcessPositionals(argv, positionalMap, self.cmdToParseOptions(commandHandler.original));
  280. return positionalMap;
  281. }
  282. function populatePositional(positional, argv, positionalMap) {
  283. const cmd = positional.cmd[0];
  284. if (positional.variadic) {
  285. positionalMap[cmd] = argv._.splice(0).map(String);
  286. }
  287. else {
  288. if (argv._.length)
  289. positionalMap[cmd] = [String(argv._.shift())];
  290. }
  291. }
  292. // we run yargs-parser against the positional arguments
  293. // applying the same parsing logic used for flags.
  294. function postProcessPositionals(argv, positionalMap, parseOptions) {
  295. // combine the parsing hints we've inferred from the command
  296. // string with explicitly configured parsing hints.
  297. const options = Object.assign({}, yargs.getOptions());
  298. options.default = Object.assign(parseOptions.default, options.default);
  299. for (const key of Object.keys(parseOptions.alias)) {
  300. options.alias[key] = (options.alias[key] || []).concat(parseOptions.alias[key]);
  301. }
  302. options.array = options.array.concat(parseOptions.array);
  303. delete options.config; // don't load config when processing positionals.
  304. const unparsed = [];
  305. Object.keys(positionalMap).forEach((key) => {
  306. positionalMap[key].map((value) => {
  307. if (options.configuration['unknown-options-as-args'])
  308. options.key[key] = true;
  309. unparsed.push(`--${key}`);
  310. unparsed.push(value);
  311. });
  312. });
  313. // short-circuit parse.
  314. if (!unparsed.length)
  315. return;
  316. const config = Object.assign({}, options.configuration, {
  317. 'populate--': true
  318. });
  319. const parsed = Parser.detailed(unparsed, Object.assign({}, options, {
  320. configuration: config
  321. }));
  322. if (parsed.error) {
  323. yargs.getUsageInstance().fail(parsed.error.message, parsed.error);
  324. }
  325. else {
  326. // only copy over positional keys (don't overwrite
  327. // flag arguments that were already parsed).
  328. const positionalKeys = Object.keys(positionalMap);
  329. Object.keys(positionalMap).forEach((key) => {
  330. positionalKeys.push(...parsed.aliases[key]);
  331. });
  332. Object.keys(parsed.argv).forEach((key) => {
  333. if (positionalKeys.indexOf(key) !== -1) {
  334. // any new aliases need to be placed in positionalMap, which
  335. // is used for validation.
  336. if (!positionalMap[key])
  337. positionalMap[key] = parsed.argv[key];
  338. argv[key] = parsed.argv[key];
  339. }
  340. });
  341. }
  342. }
  343. self.cmdToParseOptions = function (cmdString) {
  344. const parseOptions = {
  345. array: [],
  346. default: {},
  347. alias: {},
  348. demand: {}
  349. };
  350. const parsed = parse_command_1.parseCommand(cmdString);
  351. parsed.demanded.forEach((d) => {
  352. const [cmd, ...aliases] = d.cmd;
  353. if (d.variadic) {
  354. parseOptions.array.push(cmd);
  355. parseOptions.default[cmd] = [];
  356. }
  357. parseOptions.alias[cmd] = aliases;
  358. parseOptions.demand[cmd] = true;
  359. });
  360. parsed.optional.forEach((o) => {
  361. const [cmd, ...aliases] = o.cmd;
  362. if (o.variadic) {
  363. parseOptions.array.push(cmd);
  364. parseOptions.default[cmd] = [];
  365. }
  366. parseOptions.alias[cmd] = aliases;
  367. });
  368. return parseOptions;
  369. };
  370. self.reset = () => {
  371. handlers = {};
  372. aliasMap = {};
  373. defaultCommand = undefined;
  374. return self;
  375. };
  376. // used by yargs.parse() to freeze
  377. // the state of commands such that
  378. // we can apply .parse() multiple times
  379. // with the same yargs instance.
  380. const frozens = [];
  381. self.freeze = () => {
  382. frozens.push({
  383. handlers,
  384. aliasMap,
  385. defaultCommand
  386. });
  387. };
  388. self.unfreeze = () => {
  389. const frozen = frozens.pop();
  390. common_types_1.assertNotStrictEqual(frozen, undefined);
  391. ({
  392. handlers,
  393. aliasMap,
  394. defaultCommand
  395. } = frozen);
  396. };
  397. return self;
  398. }
  399. exports.command = command;
  400. function isCommandHandlerDefinition(cmd) {
  401. return typeof cmd === 'object';
  402. }
  403. exports.isCommandHandlerDefinition = isCommandHandlerDefinition;
  404. function isCommandBuilderDefinition(builder) {
  405. return typeof builder === 'object' &&
  406. !!builder.builder &&
  407. typeof builder.handler === 'function';
  408. }
  409. exports.isCommandBuilderDefinition = isCommandBuilderDefinition;
  410. function isCommandBuilderCallback(builder) {
  411. return typeof builder === 'function';
  412. }
  413. exports.isCommandBuilderCallback = isCommandBuilderCallback;
  414. function isCommandBuilderOptionDefinitions(builder) {
  415. return typeof builder === 'object';
  416. }