validation.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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)) {
  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].msg
  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) {
  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. !aliasLookup.hasOwnProperty(key)) {
  121. unknown.push(key)
  122. }
  123. })
  124. if (commandKeys.length > 0) {
  125. argv._.slice(currentContext.commands.length).forEach(function (key) {
  126. if (commandKeys.indexOf(key) === -1) {
  127. unknown.push(key)
  128. }
  129. })
  130. }
  131. if (unknown.length > 0) {
  132. usage.fail(__n(
  133. 'Unknown argument: %s',
  134. 'Unknown arguments: %s',
  135. unknown.length,
  136. unknown.join(', ')
  137. ))
  138. }
  139. }
  140. // validate arguments limited to enumerated choices
  141. self.limitedChoices = function (argv) {
  142. const options = yargs.getOptions()
  143. const invalid = {}
  144. if (!Object.keys(options.choices).length) return
  145. Object.keys(argv).forEach(function (key) {
  146. if (key !== '$0' && key !== '_' &&
  147. options.choices.hasOwnProperty(key)) {
  148. [].concat(argv[key]).forEach(function (value) {
  149. // TODO case-insensitive configurability
  150. if (options.choices[key].indexOf(value) === -1) {
  151. invalid[key] = (invalid[key] || []).concat(value)
  152. }
  153. })
  154. }
  155. })
  156. const invalidKeys = Object.keys(invalid)
  157. if (!invalidKeys.length) return
  158. var msg = __('Invalid values:')
  159. invalidKeys.forEach(function (key) {
  160. msg += '\n ' + __(
  161. 'Argument: %s, Given: %s, Choices: %s',
  162. key,
  163. usage.stringifiedValues(invalid[key]),
  164. usage.stringifiedValues(options.choices[key])
  165. )
  166. })
  167. usage.fail(msg)
  168. }
  169. // custom checks, added using the `check` option on yargs.
  170. var checks = []
  171. self.check = function (f) {
  172. checks.push(f)
  173. }
  174. self.customChecks = function (argv, aliases) {
  175. for (var i = 0, f; (f = checks[i]) !== undefined; i++) {
  176. var result = null
  177. try {
  178. result = f(argv, aliases)
  179. } catch (err) {
  180. usage.fail(err.message ? err.message : err, err)
  181. continue
  182. }
  183. if (!result) {
  184. usage.fail(__('Argument check failed: %s', f.toString()))
  185. } else if (typeof result === 'string' || result instanceof Error) {
  186. usage.fail(result.toString(), result)
  187. }
  188. }
  189. }
  190. // check implications, argument foo implies => argument bar.
  191. var implied = {}
  192. self.implies = function (key, value) {
  193. if (typeof key === 'object') {
  194. Object.keys(key).forEach(function (k) {
  195. self.implies(k, key[k])
  196. })
  197. } else {
  198. implied[key] = value
  199. }
  200. }
  201. self.getImplied = function () {
  202. return implied
  203. }
  204. self.implications = function (argv) {
  205. const implyFail = []
  206. Object.keys(implied).forEach(function (key) {
  207. var num
  208. const origKey = key
  209. var value = implied[key]
  210. // convert string '1' to number 1
  211. num = Number(key)
  212. key = isNaN(num) ? key : num
  213. if (typeof key === 'number') {
  214. // check length of argv._
  215. key = argv._.length >= key
  216. } else if (key.match(/^--no-.+/)) {
  217. // check if key doesn't exist
  218. key = key.match(/^--no-(.+)/)[1]
  219. key = !argv[key]
  220. } else {
  221. // check if key exists
  222. key = argv[key]
  223. }
  224. num = Number(value)
  225. value = isNaN(num) ? value : num
  226. if (typeof value === 'number') {
  227. value = argv._.length >= value
  228. } else if (value.match(/^--no-.+/)) {
  229. value = value.match(/^--no-(.+)/)[1]
  230. value = !argv[value]
  231. } else {
  232. value = argv[value]
  233. }
  234. if (key && !value) {
  235. implyFail.push(origKey)
  236. }
  237. })
  238. if (implyFail.length) {
  239. var msg = __('Implications failed:') + '\n'
  240. implyFail.forEach(function (key) {
  241. msg += (' ' + key + ' -> ' + implied[key])
  242. })
  243. usage.fail(msg)
  244. }
  245. }
  246. var conflicting = {}
  247. self.conflicts = function (key, value) {
  248. if (typeof key === 'object') {
  249. Object.keys(key).forEach(function (k) {
  250. self.conflicts(k, key[k])
  251. })
  252. } else {
  253. conflicting[key] = value
  254. }
  255. }
  256. self.getConflicting = function () {
  257. return conflicting
  258. }
  259. self.conflicting = function (argv) {
  260. var args = Object.getOwnPropertyNames(argv)
  261. args.forEach(function (arg) {
  262. if (conflicting[arg] && args.indexOf(conflicting[arg]) !== -1) {
  263. usage.fail(__('Arguments %s and %s are mutually exclusive', arg, conflicting[arg]))
  264. }
  265. })
  266. }
  267. self.recommendCommands = function (cmd, potentialCommands) {
  268. const distance = require('./levenshtein')
  269. const threshold = 3 // if it takes more than three edits, let's move on.
  270. potentialCommands = potentialCommands.sort(function (a, b) { return b.length - a.length })
  271. var recommended = null
  272. var bestDistance = Infinity
  273. for (var i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
  274. var d = distance(cmd, candidate)
  275. if (d <= threshold && d < bestDistance) {
  276. bestDistance = d
  277. recommended = candidate
  278. }
  279. }
  280. if (recommended) usage.fail(__('Did you mean %s?', recommended))
  281. }
  282. self.reset = function (globalLookup) {
  283. implied = objFilter(implied, function (k, v) {
  284. return globalLookup[k]
  285. })
  286. checks = []
  287. conflicting = {}
  288. return self
  289. }
  290. var frozen
  291. self.freeze = function () {
  292. frozen = {}
  293. frozen.implied = implied
  294. frozen.checks = checks
  295. frozen.conflicting = conflicting
  296. }
  297. self.unfreeze = function () {
  298. implied = frozen.implied
  299. checks = frozen.checks
  300. conflicting = frozen.conflicting
  301. frozen = undefined
  302. }
  303. return self
  304. }