parse-cli-args.js 7.1 KB

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