UNPKG

4.3 kBJavaScriptView Raw
1const optionRegex = /^--(?<option>[a-zA-Z-]+)(?:=(?<value>.+))?$/
2const shortOptionRegex = /^-(?<option>[a-zA-Z])(?<value>.+)?/
3
4class ArgumentParser {
5
6 #availableOptions = {}
7 #disableHelp = false
8 #helpText = ''
9
10 options = {}
11 args = []
12
13 constructor(args = typeof process !== 'undefined' ? process.argv.slice(2) : []) {
14 this.argsArray = args
15 }
16
17 /**
18 * @param {string} name
19 * @param {{short?: string|false, args?: number, description?: string}=} options
20 */
21 option(name, { short = name.replace('--', '')[0], args = 1, description } = {}) {
22 const option = {
23 name: name.replace('--', ''),
24 shortName: short && short.replace('-', ''),
25 args,
26 description
27 }
28 this.#availableOptions[option.name] = option
29 return this
30 }
31
32 /**
33 * @param {string} name
34 * @param {{short?: string|false, description?: string}=} options
35 */
36 flag(name, { short, description } = {}) {
37 return this.option(name, { short, args: 0, description })
38 }
39
40 help(textOrFalse) {
41 if (textOrFalse === false) {
42 this.#disableHelp = true
43 } else {
44 this.#helpText = textOrFalse
45 }
46 return this
47 }
48
49 get #helpTextToShow() {
50 return `${this.#helpText}\n\nOptions:\n${optionsToString(this.#availableOptions)}`
51 }
52
53 #addOption(target, { name }, value) {
54 if (!value) throw new Error(`option '${name}' requires a value`)
55 if (target[name]) {
56 target[name] = [].concat(target[name], value)
57 } else {
58 target[name] = value
59 }
60 }
61
62 parse({ convertCasing = true } = {}) {
63
64 const options = {}
65 const args = []
66
67 // Make a copy since we will modify it
68 const argsToParse = this.argsArray.slice(0)
69
70 for (let argToCheck = argsToParse.shift(); argToCheck; argToCheck = argsToParse.shift()) {
71 // Double dash escapes option parsing, and consumes the rest as arguments
72 if (argToCheck == '--') {
73 args.push(...argsToParse.splice(0, argsToParse.length))
74 break
75 }
76 const optionMatch = argToCheck.match(optionRegex)
77 const shortOptionMatch = argToCheck.match(shortOptionRegex)
78
79 if (optionMatch) {
80 const { groups } = optionMatch
81
82 if (groups.option == 'help' && !this.#disableHelp) return {
83 args: [],
84 options: {},
85 help: this.#helpTextToShow
86 }
87
88 const option = this.#availableOptions[groups.option]
89 if (!option) {
90 throw new Error(`Error Unknown option '${groups.option}'\n\nAvailable options:\n${optionsToString(this.#availableOptions)}`)
91 }
92 if (option.args == 0) {
93 options[option.name] = true
94 } else {
95 this.#addOption(options, option, groups.value ?? argsToParse.shift())
96 }
97 } else if (shortOptionMatch) {
98 const { groups } = shortOptionMatch
99
100 if (groups.option === 'h' && !this.#disableHelp) return {
101 args: [],
102 options: {},
103 help: this.#helpTextToShow
104 }
105
106 const option = Object.values(this.#availableOptions).find(
107 ({ shortName }) => shortName == groups.option
108 )
109 if (!option) {
110 throw new Error(`Error: Unknown switch '${groups.option}'\n\nAvailable options:\n${optionsToString(this.#availableOptions)}`)
111 }
112 if (option.args == 0) {
113 options[option.name] = true
114 if (groups.value) {
115 argsToParse.unshift(`-${groups.value}`) // Put back without first character
116 }
117 } else {
118 this.#addOption(options, option, groups.value ?? argsToParse.shift())
119 }
120 } else {
121 args.push(argToCheck)
122 }
123 }
124
125 return {
126 options: convertCasing ? convertToCamelCase(options) : options,
127 args
128 }
129 }
130}
131
132
133function optionsToString(options, omitHelp = false) {
134 const optionsToShow = Object.values(options).concat(omitHelp ? [] : {
135 name: 'help', shortName: 'h', description: 'Show this message and exit.'
136 })
137 const textsWithoutDescription = optionsToShow.map(option =>
138 ` ${option.shortName ? `-${option.shortName}, ` : ''}--${option.name}`
139 )
140 const maxLength = Math.max(...(textsWithoutDescription.map(text => text.length)))
141
142 return textsWithoutDescription.map(
143 (text, index) => `${text.padEnd(maxLength + 5)}${optionsToShow[index].description}`
144 ).join('\n')
145}
146
147function convertToCamelCase(options) {
148 return Object.fromEntries(Object.entries(options).map(
149 ([key, value]) => [
150 key.replace(/-[a-z]/, match => match.at(1).toUpperCase()),
151 value
152 ])
153 )
154}
155
156export function parseArguments(options) {
157 return new ArgumentParser(options)
158}
159