validation.js 10KB

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