validation.js 10 KB

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