validation.js 9.8 KB

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