validation.js 10 KB

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