You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
4.4 KiB

2 years ago
  1. const flagSymbol = Symbol('arg flag');
  2. class ArgError extends Error {
  3. constructor(msg, code) {
  4. super(msg);
  5. this.name = 'ArgError';
  6. this.code = code;
  7. Object.setPrototypeOf(this, ArgError.prototype);
  8. }
  9. }
  10. function arg(
  11. opts,
  12. {
  13. argv = process.argv.slice(2),
  14. permissive = false,
  15. stopAtPositional = false
  16. } = {}
  17. ) {
  18. if (!opts) {
  19. throw new ArgError(
  20. 'argument specification object is required',
  21. 'ARG_CONFIG_NO_SPEC'
  22. );
  23. }
  24. const result = { _: [] };
  25. const aliases = {};
  26. const handlers = {};
  27. for (const key of Object.keys(opts)) {
  28. if (!key) {
  29. throw new ArgError(
  30. 'argument key cannot be an empty string',
  31. 'ARG_CONFIG_EMPTY_KEY'
  32. );
  33. }
  34. if (key[0] !== '-') {
  35. throw new ArgError(
  36. `argument key must start with '-' but found: '${key}'`,
  37. 'ARG_CONFIG_NONOPT_KEY'
  38. );
  39. }
  40. if (key.length === 1) {
  41. throw new ArgError(
  42. `argument key must have a name; singular '-' keys are not allowed: ${key}`,
  43. 'ARG_CONFIG_NONAME_KEY'
  44. );
  45. }
  46. if (typeof opts[key] === 'string') {
  47. aliases[key] = opts[key];
  48. continue;
  49. }
  50. let type = opts[key];
  51. let isFlag = false;
  52. if (
  53. Array.isArray(type) &&
  54. type.length === 1 &&
  55. typeof type[0] === 'function'
  56. ) {
  57. const [fn] = type;
  58. type = (value, name, prev = []) => {
  59. prev.push(fn(value, name, prev[prev.length - 1]));
  60. return prev;
  61. };
  62. isFlag = fn === Boolean || fn[flagSymbol] === true;
  63. } else if (typeof type === 'function') {
  64. isFlag = type === Boolean || type[flagSymbol] === true;
  65. } else {
  66. throw new ArgError(
  67. `type missing or not a function or valid array type: ${key}`,
  68. 'ARG_CONFIG_VAD_TYPE'
  69. );
  70. }
  71. if (key[1] !== '-' && key.length > 2) {
  72. throw new ArgError(
  73. `short argument keys (with a single hyphen) must have only one character: ${key}`,
  74. 'ARG_CONFIG_SHORTOPT_TOOLONG'
  75. );
  76. }
  77. handlers[key] = [type, isFlag];
  78. }
  79. for (let i = 0, len = argv.length; i < len; i++) {
  80. const wholeArg = argv[i];
  81. if (stopAtPositional && result._.length > 0) {
  82. result._ = result._.concat(argv.slice(i));
  83. break;
  84. }
  85. if (wholeArg === '--') {
  86. result._ = result._.concat(argv.slice(i + 1));
  87. break;
  88. }
  89. if (wholeArg.length > 1 && wholeArg[0] === '-') {
  90. /* eslint-disable operator-linebreak */
  91. const separatedArguments =
  92. wholeArg[1] === '-' || wholeArg.length === 2
  93. ? [wholeArg]
  94. : wholeArg
  95. .slice(1)
  96. .split('')
  97. .map((a) => `-${a}`);
  98. /* eslint-enable operator-linebreak */
  99. for (let j = 0; j < separatedArguments.length; j++) {
  100. const arg = separatedArguments[j];
  101. const [originalArgName, argStr] =
  102. arg[1] === '-' ? arg.split(/=(.*)/, 2) : [arg, undefined];
  103. let argName = originalArgName;
  104. while (argName in aliases) {
  105. argName = aliases[argName];
  106. }
  107. if (!(argName in handlers)) {
  108. if (permissive) {
  109. result._.push(arg);
  110. continue;
  111. } else {
  112. throw new ArgError(
  113. `unknown or unexpected option: ${originalArgName}`,
  114. 'ARG_UNKNOWN_OPTION'
  115. );
  116. }
  117. }
  118. const [type, isFlag] = handlers[argName];
  119. if (!isFlag && j + 1 < separatedArguments.length) {
  120. throw new ArgError(
  121. `option requires argument (but was followed by another short argument): ${originalArgName}`,
  122. 'ARG_MISSING_REQUIRED_SHORTARG'
  123. );
  124. }
  125. if (isFlag) {
  126. result[argName] = type(true, argName, result[argName]);
  127. } else if (argStr === undefined) {
  128. if (
  129. argv.length < i + 2 ||
  130. (argv[i + 1].length > 1 &&
  131. argv[i + 1][0] === '-' &&
  132. !(
  133. argv[i + 1].match(/^-?\d*(\.(?=\d))?\d*$/) &&
  134. (type === Number ||
  135. // eslint-disable-next-line no-undef
  136. (typeof BigInt !== 'undefined' && type === BigInt))
  137. ))
  138. ) {
  139. const extended =
  140. originalArgName === argName ? '' : ` (alias for ${argName})`;
  141. throw new ArgError(
  142. `option requires argument: ${originalArgName}${extended}`,
  143. 'ARG_MISSING_REQUIRED_LONGARG'
  144. );
  145. }
  146. result[argName] = type(argv[i + 1], argName, result[argName]);
  147. ++i;
  148. } else {
  149. result[argName] = type(argStr, argName, result[argName]);
  150. }
  151. }
  152. } else {
  153. result._.push(wholeArg);
  154. }
  155. }
  156. return result;
  157. }
  158. arg.flag = (fn) => {
  159. fn[flagSymbol] = true;
  160. return fn;
  161. };
  162. // Utility types
  163. arg.COUNT = arg.flag((v, name, existingCount) => (existingCount || 0) + 1);
  164. // Expose error class
  165. arg.ArgError = ArgError;
  166. module.exports = arg;