parse-cli-args.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * @author Toru Nagashima
  3. * @copyright 2016 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. // ------------------------------------------------------------------------------
  8. // Helpers
  9. // ------------------------------------------------------------------------------
  10. const OVERWRITE_OPTION = /^--([^:]+?):([^=]+?)(?:=(.+))?$/
  11. const CONFIG_OPTION = /^--([^=]+?)(?:=(.+))$/
  12. const PACKAGE_CONFIG_PATTERN = /^npm_package_config_(.+)$/
  13. const CONCAT_OPTIONS = /^-[clnprs]+$/
  14. /**
  15. * Overwrites a specified package config.
  16. *
  17. * @param {object} config - A config object to be overwritten.
  18. * @param {string} packageName - A package name to overwrite.
  19. * @param {string} variable - A variable name to overwrite.
  20. * @param {string} value - A new value to overwrite.
  21. * @returns {void}
  22. */
  23. function overwriteConfig (config, packageName, variable, value) {
  24. const scope = config[packageName] || (config[packageName] = {})
  25. scope[variable] = value
  26. }
  27. /**
  28. * Creates a package config object.
  29. * This checks `process.env` and creates the default value.
  30. *
  31. * @returns {object} Created config object.
  32. */
  33. function createPackageConfig () {
  34. const retv = {}
  35. const packageName = process.env.npm_package_name
  36. if (!packageName) {
  37. return retv
  38. }
  39. for (const key of Object.keys(process.env)) {
  40. const m = PACKAGE_CONFIG_PATTERN.exec(key)
  41. if (m != null) {
  42. overwriteConfig(retv, packageName, m[1], process.env[key])
  43. }
  44. }
  45. return retv
  46. }
  47. /**
  48. * Adds a new group into a given list.
  49. *
  50. * @param {object[]} groups - A group list to add.
  51. * @param {object} initialValues - A key-value map for the default of new value.
  52. * @returns {void}
  53. */
  54. function addGroup (groups, initialValues) {
  55. groups.push(Object.assign(
  56. { parallel: false, patterns: [] },
  57. initialValues || {}
  58. ))
  59. }
  60. /**
  61. * ArgumentSet is values of parsed CLI arguments.
  62. * This class provides the getter to get the last group.
  63. */
  64. class ArgumentSet {
  65. /**
  66. * @param {object} initialValues - A key-value map for the default of new value.
  67. * @param {object} options - A key-value map for the options.
  68. */
  69. constructor (initialValues, options) {
  70. this.config = {}
  71. this.continueOnError = false
  72. this.groups = []
  73. this.maxParallel = 0
  74. this.npmPath = null
  75. this.packageConfig = createPackageConfig()
  76. this.printLabel = false
  77. this.printName = false
  78. this.race = false
  79. this.rest = []
  80. this.silent = process.env.npm_config_loglevel === 'silent'
  81. this.singleMode = Boolean(options && options.singleMode)
  82. addGroup(this.groups, initialValues)
  83. }
  84. /**
  85. * Gets the last group.
  86. */
  87. get lastGroup () {
  88. return this.groups[this.groups.length - 1]
  89. }
  90. /**
  91. * Gets "parallel" flag.
  92. */
  93. get parallel () {
  94. return this.groups.some(g => g.parallel)
  95. }
  96. }
  97. /**
  98. * Parses CLI arguments.
  99. *
  100. * @param {ArgumentSet} set - The parsed CLI arguments.
  101. * @param {string[]} args - CLI arguments.
  102. * @returns {ArgumentSet} set itself.
  103. */
  104. function parseCLIArgsCore (set, args) {
  105. LOOP: // eslint-disable-line no-labels
  106. for (let i = 0; i < args.length; ++i) {
  107. const arg = args[i]
  108. switch (arg) {
  109. case '--':
  110. set.rest = args.slice(1 + i)
  111. break LOOP // eslint-disable-line no-labels
  112. case '--color':
  113. case '--no-color':
  114. // do nothing.
  115. break
  116. case '-c':
  117. case '--continue-on-error':
  118. set.continueOnError = true
  119. break
  120. case '-l':
  121. case '--print-label':
  122. set.printLabel = true
  123. break
  124. case '-n':
  125. case '--print-name':
  126. set.printName = true
  127. break
  128. case '-r':
  129. case '--race':
  130. set.race = true
  131. break
  132. case '--silent':
  133. set.silent = true
  134. break
  135. case '--max-parallel':
  136. set.maxParallel = parseInt(args[++i], 10)
  137. if (!Number.isFinite(set.maxParallel) || set.maxParallel <= 0) {
  138. throw new Error(`Invalid Option: --max-parallel ${args[i]}`)
  139. }
  140. break
  141. case '-s':
  142. case '--sequential':
  143. case '--serial':
  144. if (set.singleMode && arg === '-s') {
  145. set.silent = true
  146. break
  147. }
  148. if (set.singleMode) {
  149. throw new Error(`Invalid Option: ${arg}`)
  150. }
  151. addGroup(set.groups)
  152. break
  153. case '--aggregate-output':
  154. set.aggregateOutput = true
  155. break
  156. case '-p':
  157. case '--parallel':
  158. if (set.singleMode) {
  159. throw new Error(`Invalid Option: ${arg}`)
  160. }
  161. addGroup(set.groups, { parallel: true })
  162. break
  163. case '--npm-path':
  164. set.npmPath = args[++i] || null
  165. break
  166. default: {
  167. let matched = null
  168. if ((matched = OVERWRITE_OPTION.exec(arg))) {
  169. overwriteConfig(
  170. set.packageConfig,
  171. matched[1],
  172. matched[2],
  173. matched[3] || args[++i]
  174. )
  175. } else if ((matched = CONFIG_OPTION.exec(arg))) {
  176. set.config[matched[1]] = matched[2]
  177. } else if (CONCAT_OPTIONS.test(arg)) {
  178. parseCLIArgsCore(
  179. set,
  180. arg.slice(1).split('').map(c => `-${c}`)
  181. )
  182. } else if (arg[0] === '-') {
  183. throw new Error(`Invalid Option: ${arg}`)
  184. } else {
  185. set.lastGroup.patterns.push(arg)
  186. }
  187. break
  188. }
  189. }
  190. }
  191. if (!set.parallel && set.aggregateOutput) {
  192. throw new Error('Invalid Option: --aggregate-output (without parallel)')
  193. }
  194. if (!set.parallel && set.race) {
  195. const race = args.indexOf('--race') !== -1 ? '--race' : '-r'
  196. throw new Error(`Invalid Option: ${race} (without parallel)`)
  197. }
  198. if (!set.parallel && set.maxParallel !== 0) {
  199. throw new Error('Invalid Option: --max-parallel (without parallel)')
  200. }
  201. return set
  202. }
  203. /**
  204. * Parses CLI arguments.
  205. *
  206. * @param {string[]} args - CLI arguments.
  207. * @param {object} initialValues - A key-value map for the default of new value.
  208. * @param {object} options - A key-value map for the options.
  209. * @param {boolean} options.singleMode - The flag to be single group mode.
  210. * @returns {ArgumentSet} The parsed CLI arguments.
  211. */
  212. module.exports = function parseCLIArgs (args, initialValues, options) {
  213. return parseCLIArgsCore(new ArgumentSet(initialValues, options), args)
  214. }