validation.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.validation = void 0;
  4. const argsert_1 = require("./argsert");
  5. const common_types_1 = require("./common-types");
  6. const levenshtein_1 = require("./levenshtein");
  7. const obj_filter_1 = require("./obj-filter");
  8. const specialKeys = ['$0', '--', '_'];
  9. // validation-type-stuff, missing params,
  10. // bad implications, custom checks.
  11. function validation(yargs, usage, y18n) {
  12. const __ = y18n.__;
  13. const __n = y18n.__n;
  14. const self = {};
  15. // validate appropriate # of non-option
  16. // arguments were provided, i.e., '_'.
  17. self.nonOptionCount = function nonOptionCount(argv) {
  18. const demandedCommands = yargs.getDemandedCommands();
  19. // don't count currently executing commands
  20. const _s = argv._.length - yargs.getContext().commands.length;
  21. if (demandedCommands._ && (_s < demandedCommands._.min || _s > demandedCommands._.max)) {
  22. if (_s < demandedCommands._.min) {
  23. if (demandedCommands._.minMsg !== undefined) {
  24. usage.fail(
  25. // replace $0 with observed, $1 with expected.
  26. demandedCommands._.minMsg
  27. ? demandedCommands._.minMsg.replace(/\$0/g, _s.toString()).replace(/\$1/, demandedCommands._.min.toString())
  28. : null);
  29. }
  30. else {
  31. usage.fail(__n('Not enough non-option arguments: got %s, need at least %s', 'Not enough non-option arguments: got %s, need at least %s', _s, _s, demandedCommands._.min));
  32. }
  33. }
  34. else if (_s > demandedCommands._.max) {
  35. if (demandedCommands._.maxMsg !== undefined) {
  36. usage.fail(
  37. // replace $0 with observed, $1 with expected.
  38. demandedCommands._.maxMsg
  39. ? demandedCommands._.maxMsg.replace(/\$0/g, _s.toString()).replace(/\$1/, demandedCommands._.max.toString())
  40. : null);
  41. }
  42. else {
  43. usage.fail(__n('Too many non-option arguments: got %s, maximum of %s', 'Too many non-option arguments: got %s, maximum of %s', _s, _s, demandedCommands._.max));
  44. }
  45. }
  46. }
  47. };
  48. // validate the appropriate # of <required>
  49. // positional arguments were provided:
  50. self.positionalCount = function positionalCount(required, observed) {
  51. if (observed < required) {
  52. usage.fail(__n('Not enough non-option arguments: got %s, need at least %s', 'Not enough non-option arguments: got %s, need at least %s', observed, observed, required));
  53. }
  54. };
  55. // make sure all the required arguments are present.
  56. self.requiredArguments = function requiredArguments(argv) {
  57. const demandedOptions = yargs.getDemandedOptions();
  58. let missing = null;
  59. for (const key of Object.keys(demandedOptions)) {
  60. if (!Object.prototype.hasOwnProperty.call(argv, key) || typeof argv[key] === 'undefined') {
  61. missing = missing || {};
  62. missing[key] = demandedOptions[key];
  63. }
  64. }
  65. if (missing) {
  66. const customMsgs = [];
  67. for (const key of Object.keys(missing)) {
  68. const msg = missing[key];
  69. if (msg && customMsgs.indexOf(msg) < 0) {
  70. customMsgs.push(msg);
  71. }
  72. }
  73. const customMsg = customMsgs.length ? `\n${customMsgs.join('\n')}` : '';
  74. usage.fail(__n('Missing required argument: %s', 'Missing required arguments: %s', Object.keys(missing).length, Object.keys(missing).join(', ') + customMsg));
  75. }
  76. };
  77. // check for unknown arguments (strict-mode).
  78. self.unknownArguments = function unknownArguments(argv, aliases, positionalMap, isDefaultCommand) {
  79. const commandKeys = yargs.getCommandInstance().getCommands();
  80. const unknown = [];
  81. const currentContext = yargs.getContext();
  82. Object.keys(argv).forEach((key) => {
  83. if (specialKeys.indexOf(key) === -1 &&
  84. !Object.prototype.hasOwnProperty.call(positionalMap, key) &&
  85. !Object.prototype.hasOwnProperty.call(yargs._getParseContext(), key) &&
  86. !self.isValidAndSomeAliasIsNotNew(key, aliases)) {
  87. unknown.push(key);
  88. }
  89. });
  90. if ((currentContext.commands.length > 0) || (commandKeys.length > 0) || isDefaultCommand) {
  91. argv._.slice(currentContext.commands.length).forEach((key) => {
  92. if (commandKeys.indexOf(key) === -1) {
  93. unknown.push(key);
  94. }
  95. });
  96. }
  97. if (unknown.length > 0) {
  98. usage.fail(__n('Unknown argument: %s', 'Unknown arguments: %s', unknown.length, unknown.join(', ')));
  99. }
  100. };
  101. self.unknownCommands = function unknownCommands(argv) {
  102. const commandKeys = yargs.getCommandInstance().getCommands();
  103. const unknown = [];
  104. const currentContext = yargs.getContext();
  105. if ((currentContext.commands.length > 0) || (commandKeys.length > 0)) {
  106. argv._.slice(currentContext.commands.length).forEach((key) => {
  107. if (commandKeys.indexOf(key) === -1) {
  108. unknown.push(key);
  109. }
  110. });
  111. }
  112. if (unknown.length > 0) {
  113. usage.fail(__n('Unknown command: %s', 'Unknown commands: %s', unknown.length, unknown.join(', ')));
  114. return true;
  115. }
  116. else {
  117. return false;
  118. }
  119. };
  120. // check for a key that is not an alias, or for which every alias is new,
  121. // implying that it was invented by the parser, e.g., during camelization
  122. self.isValidAndSomeAliasIsNotNew = function isValidAndSomeAliasIsNotNew(key, aliases) {
  123. if (!Object.prototype.hasOwnProperty.call(aliases, key)) {
  124. return false;
  125. }
  126. const newAliases = yargs.parsed.newAliases;
  127. for (const a of [key, ...aliases[key]]) {
  128. if (!Object.prototype.hasOwnProperty.call(newAliases, a) || !newAliases[key]) {
  129. return true;
  130. }
  131. }
  132. return false;
  133. };
  134. // validate arguments limited to enumerated choices
  135. self.limitedChoices = function limitedChoices(argv) {
  136. const options = yargs.getOptions();
  137. const invalid = {};
  138. if (!Object.keys(options.choices).length)
  139. return;
  140. Object.keys(argv).forEach((key) => {
  141. if (specialKeys.indexOf(key) === -1 &&
  142. Object.prototype.hasOwnProperty.call(options.choices, key)) {
  143. [].concat(argv[key]).forEach((value) => {
  144. // TODO case-insensitive configurability
  145. if (options.choices[key].indexOf(value) === -1 &&
  146. value !== undefined) {
  147. invalid[key] = (invalid[key] || []).concat(value);
  148. }
  149. });
  150. }
  151. });
  152. const invalidKeys = Object.keys(invalid);
  153. if (!invalidKeys.length)
  154. return;
  155. let msg = __('Invalid values:');
  156. invalidKeys.forEach((key) => {
  157. msg += `\n ${__('Argument: %s, Given: %s, Choices: %s', key, usage.stringifiedValues(invalid[key]), usage.stringifiedValues(options.choices[key]))}`;
  158. });
  159. usage.fail(msg);
  160. };
  161. // custom checks, added using the `check` option on yargs.
  162. let checks = [];
  163. self.check = function check(f, global) {
  164. checks.push({
  165. func: f,
  166. global
  167. });
  168. };
  169. self.customChecks = function customChecks(argv, aliases) {
  170. for (let i = 0, f; (f = checks[i]) !== undefined; i++) {
  171. const func = f.func;
  172. let result = null;
  173. try {
  174. result = func(argv, aliases);
  175. }
  176. catch (err) {
  177. usage.fail(err.message ? err.message : err, err);
  178. continue;
  179. }
  180. if (!result) {
  181. usage.fail(__('Argument check failed: %s', func.toString()));
  182. }
  183. else if (typeof result === 'string' || result instanceof Error) {
  184. usage.fail(result.toString(), result);
  185. }
  186. }
  187. };
  188. // check implications, argument foo implies => argument bar.
  189. let implied = {};
  190. self.implies = function implies(key, value) {
  191. argsert_1.argsert('<string|object> [array|number|string]', [key, value], arguments.length);
  192. if (typeof key === 'object') {
  193. Object.keys(key).forEach((k) => {
  194. self.implies(k, key[k]);
  195. });
  196. }
  197. else {
  198. yargs.global(key);
  199. if (!implied[key]) {
  200. implied[key] = [];
  201. }
  202. if (Array.isArray(value)) {
  203. value.forEach((i) => self.implies(key, i));
  204. }
  205. else {
  206. common_types_1.assertNotStrictEqual(value, undefined);
  207. implied[key].push(value);
  208. }
  209. }
  210. };
  211. self.getImplied = function getImplied() {
  212. return implied;
  213. };
  214. function keyExists(argv, val) {
  215. // convert string '1' to number 1
  216. const num = Number(val);
  217. val = isNaN(num) ? val : num;
  218. if (typeof val === 'number') {
  219. // check length of argv._
  220. val = argv._.length >= val;
  221. }
  222. else if (val.match(/^--no-.+/)) {
  223. // check if key/value doesn't exist
  224. val = val.match(/^--no-(.+)/)[1];
  225. val = !argv[val];
  226. }
  227. else {
  228. // check if key/value exists
  229. val = argv[val];
  230. }
  231. return val;
  232. }
  233. self.implications = function implications(argv) {
  234. const implyFail = [];
  235. Object.keys(implied).forEach((key) => {
  236. const origKey = key;
  237. (implied[key] || []).forEach((value) => {
  238. let key = origKey;
  239. const origValue = value;
  240. key = keyExists(argv, key);
  241. value = keyExists(argv, value);
  242. if (key && !value) {
  243. implyFail.push(` ${origKey} -> ${origValue}`);
  244. }
  245. });
  246. });
  247. if (implyFail.length) {
  248. let msg = `${__('Implications failed:')}\n`;
  249. implyFail.forEach((value) => {
  250. msg += (value);
  251. });
  252. usage.fail(msg);
  253. }
  254. };
  255. let conflicting = {};
  256. self.conflicts = function conflicts(key, value) {
  257. argsert_1.argsert('<string|object> [array|string]', [key, value], arguments.length);
  258. if (typeof key === 'object') {
  259. Object.keys(key).forEach((k) => {
  260. self.conflicts(k, key[k]);
  261. });
  262. }
  263. else {
  264. yargs.global(key);
  265. if (!conflicting[key]) {
  266. conflicting[key] = [];
  267. }
  268. if (Array.isArray(value)) {
  269. value.forEach((i) => self.conflicts(key, i));
  270. }
  271. else {
  272. conflicting[key].push(value);
  273. }
  274. }
  275. };
  276. self.getConflicting = () => conflicting;
  277. self.conflicting = function conflictingFn(argv) {
  278. Object.keys(argv).forEach((key) => {
  279. if (conflicting[key]) {
  280. conflicting[key].forEach((value) => {
  281. // we default keys to 'undefined' that have been configured, we should not
  282. // apply conflicting check unless they are a value other than 'undefined'.
  283. if (value && argv[key] !== undefined && argv[value] !== undefined) {
  284. usage.fail(__('Arguments %s and %s are mutually exclusive', key, value));
  285. }
  286. });
  287. }
  288. });
  289. };
  290. self.recommendCommands = function recommendCommands(cmd, potentialCommands) {
  291. const threshold = 3; // if it takes more than three edits, let's move on.
  292. potentialCommands = potentialCommands.sort((a, b) => b.length - a.length);
  293. let recommended = null;
  294. let bestDistance = Infinity;
  295. for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
  296. const d = levenshtein_1.levenshtein(cmd, candidate);
  297. if (d <= threshold && d < bestDistance) {
  298. bestDistance = d;
  299. recommended = candidate;
  300. }
  301. }
  302. if (recommended)
  303. usage.fail(__('Did you mean %s?', recommended));
  304. };
  305. self.reset = function reset(localLookup) {
  306. implied = obj_filter_1.objFilter(implied, k => !localLookup[k]);
  307. conflicting = obj_filter_1.objFilter(conflicting, k => !localLookup[k]);
  308. checks = checks.filter(c => c.global);
  309. return self;
  310. };
  311. const frozens = [];
  312. self.freeze = function freeze() {
  313. frozens.push({
  314. implied,
  315. checks,
  316. conflicting
  317. });
  318. };
  319. self.unfreeze = function unfreeze() {
  320. const frozen = frozens.pop();
  321. common_types_1.assertNotStrictEqual(frozen, undefined);
  322. ({
  323. implied,
  324. checks,
  325. conflicting
  326. } = frozen);
  327. };
  328. return self;
  329. }
  330. exports.validation = validation;