validation.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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 that any args that require an
  53. // value (--foo=bar), have a value.
  54. self.missingArgumentValue = function missingArgumentValue (argv) {
  55. const defaultValues = [true, false, '', undefined]
  56. const options = yargs.getOptions()
  57. if (options.requiresArg.length > 0) {
  58. const missingRequiredArgs = []
  59. options.requiresArg.forEach((key) => {
  60. // if the argument is not set in argv no need to check
  61. // whether a right-hand-side has been provided.
  62. if (!argv.hasOwnProperty(key)) return
  63. const value = argv[key]
  64. // if a value is explicitly requested,
  65. // flag argument as missing if it does not
  66. // look like foo=bar was entered.
  67. if (~defaultValues.indexOf(value) ||
  68. (Array.isArray(value) && !value.length)) {
  69. missingRequiredArgs.push(key)
  70. }
  71. })
  72. if (missingRequiredArgs.length > 0) {
  73. usage.fail(__n(
  74. 'Missing argument value: %s',
  75. 'Missing argument values: %s',
  76. missingRequiredArgs.length,
  77. missingRequiredArgs.join(', ')
  78. ))
  79. }
  80. }
  81. }
  82. // make sure all the required arguments are present.
  83. self.requiredArguments = function requiredArguments (argv) {
  84. const demandedOptions = yargs.getDemandedOptions()
  85. let missing = null
  86. Object.keys(demandedOptions).forEach((key) => {
  87. if (!argv.hasOwnProperty(key) || typeof argv[key] === 'undefined') {
  88. missing = missing || {}
  89. missing[key] = demandedOptions[key]
  90. }
  91. })
  92. if (missing) {
  93. const customMsgs = []
  94. Object.keys(missing).forEach((key) => {
  95. const msg = missing[key]
  96. if (msg && customMsgs.indexOf(msg) < 0) {
  97. customMsgs.push(msg)
  98. }
  99. })
  100. const customMsg = customMsgs.length ? `\n${customMsgs.join('\n')}` : ''
  101. usage.fail(__n(
  102. 'Missing required argument: %s',
  103. 'Missing required arguments: %s',
  104. Object.keys(missing).length,
  105. Object.keys(missing).join(', ') + customMsg
  106. ))
  107. }
  108. }
  109. // check for unknown arguments (strict-mode).
  110. self.unknownArguments = function unknownArguments (argv, aliases, positionalMap) {
  111. const commandKeys = yargs.getCommandInstance().getCommands()
  112. const unknown = []
  113. const currentContext = yargs.getContext()
  114. Object.keys(argv).forEach((key) => {
  115. if (specialKeys.indexOf(key) === -1 &&
  116. !positionalMap.hasOwnProperty(key) &&
  117. !yargs._getParseContext().hasOwnProperty(key) &&
  118. !aliases.hasOwnProperty(key)
  119. ) {
  120. unknown.push(key)
  121. }
  122. })
  123. if (commandKeys.length > 0) {
  124. argv._.slice(currentContext.commands.length).forEach((key) => {
  125. if (commandKeys.indexOf(key) === -1) {
  126. unknown.push(key)
  127. }
  128. })
  129. }
  130. if (unknown.length > 0) {
  131. usage.fail(__n(
  132. 'Unknown argument: %s',
  133. 'Unknown arguments: %s',
  134. unknown.length,
  135. unknown.join(', ')
  136. ))
  137. }
  138. }
  139. // validate arguments limited to enumerated choices
  140. self.limitedChoices = function limitedChoices (argv) {
  141. const options = yargs.getOptions()
  142. const invalid = {}
  143. if (!Object.keys(options.choices).length) return
  144. Object.keys(argv).forEach((key) => {
  145. if (specialKeys.indexOf(key) === -1 &&
  146. options.choices.hasOwnProperty(key)) {
  147. [].concat(argv[key]).forEach((value) => {
  148. // TODO case-insensitive configurability
  149. if (options.choices[key].indexOf(value) === -1 &&
  150. value !== undefined) {
  151. invalid[key] = (invalid[key] || []).concat(value)
  152. }
  153. })
  154. }
  155. })
  156. const invalidKeys = Object.keys(invalid)
  157. if (!invalidKeys.length) return
  158. let msg = __('Invalid values:')
  159. invalidKeys.forEach((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. let checks = []
  171. self.check = function check (f, global) {
  172. checks.push({
  173. func: f,
  174. global
  175. })
  176. }
  177. self.customChecks = function customChecks (argv, aliases) {
  178. for (let i = 0, f; (f = checks[i]) !== undefined; i++) {
  179. const func = f.func
  180. let result = null
  181. try {
  182. result = func(argv, aliases)
  183. } catch (err) {
  184. usage.fail(err.message ? err.message : err, err)
  185. continue
  186. }
  187. if (!result) {
  188. usage.fail(__('Argument check failed: %s', func.toString()))
  189. } else if (typeof result === 'string' || result instanceof Error) {
  190. usage.fail(result.toString(), result)
  191. }
  192. }
  193. }
  194. // check implications, argument foo implies => argument bar.
  195. let implied = {}
  196. self.implies = function implies (key, value) {
  197. argsert('<string|object> [array|number|string]', [key, value], arguments.length)
  198. if (typeof key === 'object') {
  199. Object.keys(key).forEach((k) => {
  200. self.implies(k, key[k])
  201. })
  202. } else {
  203. yargs.global(key)
  204. if (!implied[key]) {
  205. implied[key] = []
  206. }
  207. if (Array.isArray(value)) {
  208. value.forEach((i) => self.implies(key, i))
  209. } else {
  210. implied[key].push(value)
  211. }
  212. }
  213. }
  214. self.getImplied = function getImplied () {
  215. return implied
  216. }
  217. self.implications = function implications (argv) {
  218. const implyFail = []
  219. Object.keys(implied).forEach((key) => {
  220. const origKey = key
  221. ;(implied[key] || []).forEach((value) => {
  222. let num
  223. let key = origKey
  224. const origValue = value
  225. // convert string '1' to number 1
  226. num = Number(key)
  227. key = isNaN(num) ? key : num
  228. if (typeof key === 'number') {
  229. // check length of argv._
  230. key = argv._.length >= key
  231. } else if (key.match(/^--no-.+/)) {
  232. // check if key doesn't exist
  233. key = key.match(/^--no-(.+)/)[1]
  234. key = !argv[key]
  235. } else {
  236. // check if key exists
  237. key = argv[key]
  238. }
  239. num = Number(value)
  240. value = isNaN(num) ? value : num
  241. if (typeof value === 'number') {
  242. value = argv._.length >= value
  243. } else if (value.match(/^--no-.+/)) {
  244. value = value.match(/^--no-(.+)/)[1]
  245. value = !argv[value]
  246. } else {
  247. value = argv[value]
  248. }
  249. if (key && !value) {
  250. implyFail.push(` ${origKey} -> ${origValue}`)
  251. }
  252. })
  253. })
  254. if (implyFail.length) {
  255. let msg = `${__('Implications failed:')}\n`
  256. implyFail.forEach((value) => {
  257. msg += (value)
  258. })
  259. usage.fail(msg)
  260. }
  261. }
  262. let conflicting = {}
  263. self.conflicts = function conflicts (key, value) {
  264. argsert('<string|object> [array|string]', [key, value], arguments.length)
  265. if (typeof key === 'object') {
  266. Object.keys(key).forEach((k) => {
  267. self.conflicts(k, key[k])
  268. })
  269. } else {
  270. yargs.global(key)
  271. if (!conflicting[key]) {
  272. conflicting[key] = []
  273. }
  274. if (Array.isArray(value)) {
  275. value.forEach((i) => self.conflicts(key, i))
  276. } else {
  277. conflicting[key].push(value)
  278. }
  279. }
  280. }
  281. self.getConflicting = () => conflicting
  282. self.conflicting = function conflictingFn (argv) {
  283. Object.keys(argv).forEach((key) => {
  284. if (conflicting[key]) {
  285. conflicting[key].forEach((value) => {
  286. // we default keys to 'undefined' that have been configured, we should not
  287. // apply conflicting check unless they are a value other than 'undefined'.
  288. if (value && argv[key] !== undefined && argv[value] !== undefined) {
  289. usage.fail(__(`Arguments ${key} and ${value} are mutually exclusive`))
  290. }
  291. })
  292. }
  293. })
  294. }
  295. self.recommendCommands = function recommendCommands (cmd, potentialCommands) {
  296. const distance = require('./levenshtein')
  297. const threshold = 3 // if it takes more than three edits, let's move on.
  298. potentialCommands = potentialCommands.sort((a, b) => b.length - a.length)
  299. let recommended = null
  300. let bestDistance = Infinity
  301. for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
  302. const d = distance(cmd, candidate)
  303. if (d <= threshold && d < bestDistance) {
  304. bestDistance = d
  305. recommended = candidate
  306. }
  307. }
  308. if (recommended) usage.fail(__('Did you mean %s?', recommended))
  309. }
  310. self.reset = function reset (localLookup) {
  311. implied = objFilter(implied, (k, v) => !localLookup[k])
  312. conflicting = objFilter(conflicting, (k, v) => !localLookup[k])
  313. checks = checks.filter(c => c.global)
  314. return self
  315. }
  316. let frozen
  317. self.freeze = function freeze () {
  318. frozen = {}
  319. frozen.implied = implied
  320. frozen.checks = checks
  321. frozen.conflicting = conflicting
  322. }
  323. self.unfreeze = function unfreeze () {
  324. implied = frozen.implied
  325. checks = frozen.checks
  326. conflicting = frozen.conflicting
  327. frozen = undefined
  328. }
  329. return self
  330. }