No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

command.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { assertNotStrictEqual, } from './typings/common-types.js';
  2. import { isPromise } from './utils/is-promise.js';
  3. import { applyMiddleware, commandMiddlewareFactory, } from './middleware.js';
  4. import { parseCommand } from './parse-command.js';
  5. import { isYargsInstance, } from './yargs-factory.js';
  6. import whichModule from './utils/which-module.js';
  7. const DEFAULT_MARKER = /(^\*)|(^\$0)/;
  8. export function command(yargs, usage, validation, globalMiddleware = [], shim) {
  9. const self = {};
  10. let handlers = {};
  11. let aliasMap = {};
  12. let defaultCommand;
  13. self.addHandler = function addHandler(cmd, description, builder, handler, commandMiddleware, deprecated) {
  14. let aliases = [];
  15. const middlewares = commandMiddlewareFactory(commandMiddleware);
  16. handler = handler || (() => { });
  17. if (Array.isArray(cmd)) {
  18. if (isCommandAndAliases(cmd)) {
  19. [cmd, ...aliases] = cmd;
  20. }
  21. else {
  22. for (const command of cmd) {
  23. self.addHandler(command);
  24. }
  25. }
  26. }
  27. else if (isCommandHandlerDefinition(cmd)) {
  28. let command = Array.isArray(cmd.command) || typeof cmd.command === 'string'
  29. ? cmd.command
  30. : moduleName(cmd);
  31. if (cmd.aliases)
  32. command = [].concat(command).concat(cmd.aliases);
  33. self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler, cmd.middlewares, cmd.deprecated);
  34. return;
  35. }
  36. else if (isCommandBuilderDefinition(builder)) {
  37. self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares, builder.deprecated);
  38. return;
  39. }
  40. if (typeof cmd === 'string') {
  41. const parsedCommand = parseCommand(cmd);
  42. aliases = aliases.map(alias => parseCommand(alias).cmd);
  43. let isDefault = false;
  44. const parsedAliases = [parsedCommand.cmd].concat(aliases).filter(c => {
  45. if (DEFAULT_MARKER.test(c)) {
  46. isDefault = true;
  47. return false;
  48. }
  49. return true;
  50. });
  51. if (parsedAliases.length === 0 && isDefault)
  52. parsedAliases.push('$0');
  53. if (isDefault) {
  54. parsedCommand.cmd = parsedAliases[0];
  55. aliases = parsedAliases.slice(1);
  56. cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
  57. }
  58. aliases.forEach(alias => {
  59. aliasMap[alias] = parsedCommand.cmd;
  60. });
  61. if (description !== false) {
  62. usage.command(cmd, description, isDefault, aliases, deprecated);
  63. }
  64. handlers[parsedCommand.cmd] = {
  65. original: cmd,
  66. description,
  67. handler,
  68. builder: builder || {},
  69. middlewares,
  70. deprecated,
  71. demanded: parsedCommand.demanded,
  72. optional: parsedCommand.optional,
  73. };
  74. if (isDefault)
  75. defaultCommand = handlers[parsedCommand.cmd];
  76. }
  77. };
  78. self.addDirectory = function addDirectory(dir, context, req, callerFile, opts) {
  79. opts = opts || {};
  80. if (typeof opts.recurse !== 'boolean')
  81. opts.recurse = false;
  82. if (!Array.isArray(opts.extensions))
  83. opts.extensions = ['js'];
  84. const parentVisit = typeof opts.visit === 'function' ? opts.visit : (o) => o;
  85. opts.visit = function visit(obj, joined, filename) {
  86. const visited = parentVisit(obj, joined, filename);
  87. if (visited) {
  88. if (~context.files.indexOf(joined))
  89. return visited;
  90. context.files.push(joined);
  91. self.addHandler(visited);
  92. }
  93. return visited;
  94. };
  95. shim.requireDirectory({ require: req, filename: callerFile }, dir, opts);
  96. };
  97. function moduleName(obj) {
  98. const mod = whichModule(obj);
  99. if (!mod)
  100. throw new Error(`No command name given for module: ${shim.inspect(obj)}`);
  101. return commandFromFilename(mod.filename);
  102. }
  103. function commandFromFilename(filename) {
  104. return shim.path.basename(filename, shim.path.extname(filename));
  105. }
  106. function extractDesc({ describe, description, desc, }) {
  107. for (const test of [describe, description, desc]) {
  108. if (typeof test === 'string' || test === false)
  109. return test;
  110. assertNotStrictEqual(test, true, shim);
  111. }
  112. return false;
  113. }
  114. self.getCommands = () => Object.keys(handlers).concat(Object.keys(aliasMap));
  115. self.getCommandHandlers = () => handlers;
  116. self.hasDefaultCommand = () => !!defaultCommand;
  117. self.runCommand = function runCommand(command, yargs, parsed, commandIndex) {
  118. let aliases = parsed.aliases;
  119. const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand;
  120. const currentContext = yargs.getContext();
  121. let numFiles = currentContext.files.length;
  122. const parentCommands = currentContext.commands.slice();
  123. let innerArgv = parsed.argv;
  124. let positionalMap = {};
  125. if (command) {
  126. currentContext.commands.push(command);
  127. currentContext.fullCommands.push(commandHandler.original);
  128. }
  129. const builder = commandHandler.builder;
  130. if (isCommandBuilderCallback(builder)) {
  131. const builderOutput = builder(yargs.reset(parsed.aliases));
  132. const innerYargs = isYargsInstance(builderOutput) ? builderOutput : yargs;
  133. if (shouldUpdateUsage(innerYargs)) {
  134. innerYargs
  135. .getUsageInstance()
  136. .usage(usageFromParentCommandsCommandHandler(parentCommands, commandHandler), commandHandler.description);
  137. }
  138. innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
  139. aliases = innerYargs.parsed.aliases;
  140. }
  141. else if (isCommandBuilderOptionDefinitions(builder)) {
  142. const innerYargs = yargs.reset(parsed.aliases);
  143. if (shouldUpdateUsage(innerYargs)) {
  144. innerYargs
  145. .getUsageInstance()
  146. .usage(usageFromParentCommandsCommandHandler(parentCommands, commandHandler), commandHandler.description);
  147. }
  148. Object.keys(commandHandler.builder).forEach(key => {
  149. innerYargs.option(key, builder[key]);
  150. });
  151. innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
  152. aliases = innerYargs.parsed.aliases;
  153. }
  154. if (!yargs._hasOutput()) {
  155. positionalMap = populatePositionals(commandHandler, innerArgv, currentContext);
  156. }
  157. const middlewares = globalMiddleware
  158. .slice(0)
  159. .concat(commandHandler.middlewares);
  160. applyMiddleware(innerArgv, yargs, middlewares, true);
  161. if (!yargs._hasOutput()) {
  162. yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error, !command);
  163. }
  164. if (commandHandler.handler && !yargs._hasOutput()) {
  165. yargs._setHasOutput();
  166. const populateDoubleDash = !!yargs.getOptions().configuration['populate--'];
  167. yargs._postProcess(innerArgv, populateDoubleDash);
  168. innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
  169. let handlerResult;
  170. if (isPromise(innerArgv)) {
  171. handlerResult = innerArgv.then(argv => commandHandler.handler(argv));
  172. }
  173. else {
  174. handlerResult = commandHandler.handler(innerArgv);
  175. }
  176. const handlerFinishCommand = yargs.getHandlerFinishCommand();
  177. if (isPromise(handlerResult)) {
  178. yargs.getUsageInstance().cacheHelpMessage();
  179. handlerResult
  180. .then(value => {
  181. if (handlerFinishCommand) {
  182. handlerFinishCommand(value);
  183. }
  184. })
  185. .catch(error => {
  186. try {
  187. yargs.getUsageInstance().fail(null, error);
  188. }
  189. catch (err) {
  190. }
  191. })
  192. .then(() => {
  193. yargs.getUsageInstance().clearCachedHelpMessage();
  194. });
  195. }
  196. else {
  197. if (handlerFinishCommand) {
  198. handlerFinishCommand(handlerResult);
  199. }
  200. }
  201. }
  202. if (command) {
  203. currentContext.commands.pop();
  204. currentContext.fullCommands.pop();
  205. }
  206. numFiles = currentContext.files.length - numFiles;
  207. if (numFiles > 0)
  208. currentContext.files.splice(numFiles * -1, numFiles);
  209. return innerArgv;
  210. };
  211. function shouldUpdateUsage(yargs) {
  212. return (!yargs.getUsageInstance().getUsageDisabled() &&
  213. yargs.getUsageInstance().getUsage().length === 0);
  214. }
  215. function usageFromParentCommandsCommandHandler(parentCommands, commandHandler) {
  216. const c = DEFAULT_MARKER.test(commandHandler.original)
  217. ? commandHandler.original.replace(DEFAULT_MARKER, '').trim()
  218. : commandHandler.original;
  219. const pc = parentCommands.filter(c => {
  220. return !DEFAULT_MARKER.test(c);
  221. });
  222. pc.push(c);
  223. return `$0 ${pc.join(' ')}`;
  224. }
  225. self.runDefaultBuilderOn = function (yargs) {
  226. assertNotStrictEqual(defaultCommand, undefined, shim);
  227. if (shouldUpdateUsage(yargs)) {
  228. const commandString = DEFAULT_MARKER.test(defaultCommand.original)
  229. ? defaultCommand.original
  230. : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ');
  231. yargs.getUsageInstance().usage(commandString, defaultCommand.description);
  232. }
  233. const builder = defaultCommand.builder;
  234. if (isCommandBuilderCallback(builder)) {
  235. builder(yargs);
  236. }
  237. else if (!isCommandBuilderDefinition(builder)) {
  238. Object.keys(builder).forEach(key => {
  239. yargs.option(key, builder[key]);
  240. });
  241. }
  242. };
  243. function populatePositionals(commandHandler, argv, context) {
  244. argv._ = argv._.slice(context.commands.length);
  245. const demanded = commandHandler.demanded.slice(0);
  246. const optional = commandHandler.optional.slice(0);
  247. const positionalMap = {};
  248. validation.positionalCount(demanded.length, argv._.length);
  249. while (demanded.length) {
  250. const demand = demanded.shift();
  251. populatePositional(demand, argv, positionalMap);
  252. }
  253. while (optional.length) {
  254. const maybe = optional.shift();
  255. populatePositional(maybe, argv, positionalMap);
  256. }
  257. argv._ = context.commands.concat(argv._.map(a => '' + a));
  258. postProcessPositionals(argv, positionalMap, self.cmdToParseOptions(commandHandler.original));
  259. return positionalMap;
  260. }
  261. function populatePositional(positional, argv, positionalMap) {
  262. const cmd = positional.cmd[0];
  263. if (positional.variadic) {
  264. positionalMap[cmd] = argv._.splice(0).map(String);
  265. }
  266. else {
  267. if (argv._.length)
  268. positionalMap[cmd] = [String(argv._.shift())];
  269. }
  270. }
  271. function postProcessPositionals(argv, positionalMap, parseOptions) {
  272. const options = Object.assign({}, yargs.getOptions());
  273. options.default = Object.assign(parseOptions.default, options.default);
  274. for (const key of Object.keys(parseOptions.alias)) {
  275. options.alias[key] = (options.alias[key] || []).concat(parseOptions.alias[key]);
  276. }
  277. options.array = options.array.concat(parseOptions.array);
  278. options.config = {};
  279. const unparsed = [];
  280. Object.keys(positionalMap).forEach(key => {
  281. positionalMap[key].map(value => {
  282. if (options.configuration['unknown-options-as-args'])
  283. options.key[key] = true;
  284. unparsed.push(`--${key}`);
  285. unparsed.push(value);
  286. });
  287. });
  288. if (!unparsed.length)
  289. return;
  290. const config = Object.assign({}, options.configuration, {
  291. 'populate--': true,
  292. });
  293. const parsed = shim.Parser.detailed(unparsed, Object.assign({}, options, {
  294. configuration: config,
  295. }));
  296. if (parsed.error) {
  297. yargs.getUsageInstance().fail(parsed.error.message, parsed.error);
  298. }
  299. else {
  300. const positionalKeys = Object.keys(positionalMap);
  301. Object.keys(positionalMap).forEach(key => {
  302. positionalKeys.push(...parsed.aliases[key]);
  303. });
  304. Object.keys(parsed.argv).forEach(key => {
  305. if (positionalKeys.indexOf(key) !== -1) {
  306. if (!positionalMap[key])
  307. positionalMap[key] = parsed.argv[key];
  308. argv[key] = parsed.argv[key];
  309. }
  310. });
  311. }
  312. }
  313. self.cmdToParseOptions = function (cmdString) {
  314. const parseOptions = {
  315. array: [],
  316. default: {},
  317. alias: {},
  318. demand: {},
  319. };
  320. const parsed = parseCommand(cmdString);
  321. parsed.demanded.forEach(d => {
  322. const [cmd, ...aliases] = d.cmd;
  323. if (d.variadic) {
  324. parseOptions.array.push(cmd);
  325. parseOptions.default[cmd] = [];
  326. }
  327. parseOptions.alias[cmd] = aliases;
  328. parseOptions.demand[cmd] = true;
  329. });
  330. parsed.optional.forEach(o => {
  331. const [cmd, ...aliases] = o.cmd;
  332. if (o.variadic) {
  333. parseOptions.array.push(cmd);
  334. parseOptions.default[cmd] = [];
  335. }
  336. parseOptions.alias[cmd] = aliases;
  337. });
  338. return parseOptions;
  339. };
  340. self.reset = () => {
  341. handlers = {};
  342. aliasMap = {};
  343. defaultCommand = undefined;
  344. return self;
  345. };
  346. const frozens = [];
  347. self.freeze = () => {
  348. frozens.push({
  349. handlers,
  350. aliasMap,
  351. defaultCommand,
  352. });
  353. };
  354. self.unfreeze = () => {
  355. const frozen = frozens.pop();
  356. assertNotStrictEqual(frozen, undefined, shim);
  357. ({ handlers, aliasMap, defaultCommand } = frozen);
  358. };
  359. return self;
  360. }
  361. export function isCommandBuilderDefinition(builder) {
  362. return (typeof builder === 'object' &&
  363. !!builder.builder &&
  364. typeof builder.handler === 'function');
  365. }
  366. function isCommandAndAliases(cmd) {
  367. if (cmd.every(c => typeof c === 'string')) {
  368. return true;
  369. }
  370. else {
  371. return false;
  372. }
  373. }
  374. export function isCommandBuilderCallback(builder) {
  375. return typeof builder === 'function';
  376. }
  377. function isCommandBuilderOptionDefinitions(builder) {
  378. return typeof builder === 'object';
  379. }
  380. export function isCommandHandlerDefinition(cmd) {
  381. return typeof cmd === 'object' && !Array.isArray(cmd);
  382. }