1 | const optionRegex = /^--(?<option>[a-zA-Z-]+)(?:=(?<value>.+))?$/
|
2 | const shortOptionRegex = /^-(?<option>[a-zA-Z])(?<value>.+)?/
|
3 |
|
4 | class 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 |
|
19 |
|
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 |
|
34 |
|
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 |
|
68 | const argsToParse = this.argsArray.slice(0)
|
69 |
|
70 | for (let argToCheck = argsToParse.shift(); argToCheck; argToCheck = argsToParse.shift()) {
|
71 |
|
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}`)
|
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 |
|
133 | function 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 |
|
147 | function 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 |
|
156 | export function parseArguments(options) {
|
157 | return new ArgumentParser(options)
|
158 | }
|
159 |
|