@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
160 lines (134 loc) • 4.3 kB
JavaScript
const optionRegex = /^--(?<option>[a-zA-Z-]+)(?:=(?<value>.+))?$/
const shortOptionRegex = /^-(?<option>[a-zA-Z])(?<value>.+)?/
class ArgumentParser {
#availableOptions = {}
#disableHelp = false
#helpText = ''
options = {}
args = []
constructor(args = typeof process !== 'undefined' ? process.argv.slice(2) : []) {
this.argsArray = args
}
/**
* @param {string} name
* @param {{short?: string|false, args?: number, description?: string}=} options
*/
option(name, { short = name.replace('--', '')[0], args = 1, description } = {}) {
const option = {
name: name.replace('--', ''),
shortName: short && short.replace('-', ''),
args,
description
}
this.#availableOptions[option.name] = option
return this
}
/**
* @param {string} name
* @param {{short?: string|false, description?: string}=} options
*/
flag(name, { short, description } = {}) {
return this.option(name, { short, args: 0, description })
}
help(textOrFalse) {
if (textOrFalse === false) {
this.#disableHelp = true
} else {
this.#helpText = textOrFalse
}
return this
}
get #helpTextToShow() {
return `${this.#helpText}\n\nOptions:\n${optionsToString(this.#availableOptions)}`
}
#addOption(target, { name }, value) {
if (!value) throw new Error(`option '${name}' requires a value`)
if (target[name]) {
target[name] = [].concat(target[name], value)
} else {
target[name] = value
}
}
parse({ convertCasing = true } = {}) {
const options = {}
const args = []
// Make a copy since we will modify it
const argsToParse = this.argsArray.slice(0)
for (let argToCheck = argsToParse.shift(); argToCheck; argToCheck = argsToParse.shift()) {
// Double dash escapes option parsing, and consumes the rest as arguments
if (argToCheck == '--') {
args.push(...argsToParse.splice(0, argsToParse.length))
break
}
const optionMatch = argToCheck.match(optionRegex)
const shortOptionMatch = argToCheck.match(shortOptionRegex)
if (optionMatch) {
const { groups } = optionMatch
if (groups.option == 'help' && !this.#disableHelp) return {
args: [],
options: {},
help: this.#helpTextToShow
}
const option = this.#availableOptions[groups.option]
if (!option) {
throw new Error(`Error Unknown option '${groups.option}'\n\nAvailable options:\n${optionsToString(this.#availableOptions)}`)
}
if (option.args == 0) {
options[option.name] = true
} else {
this.#addOption(options, option, groups.value ?? argsToParse.shift())
}
} else if (shortOptionMatch) {
const { groups } = shortOptionMatch
if (groups.option === 'h' && !this.#disableHelp) return {
args: [],
options: {},
help: this.#helpTextToShow
}
const option = Object.values(this.#availableOptions).find(
({ shortName }) => shortName == groups.option
)
if (!option) {
throw new Error(`Error: Unknown switch '${groups.option}'\n\nAvailable options:\n${optionsToString(this.#availableOptions)}`)
}
if (option.args == 0) {
options[option.name] = true
if (groups.value) {
argsToParse.unshift(`-${groups.value}`) // Put back without first character
}
} else {
this.#addOption(options, option, groups.value ?? argsToParse.shift())
}
} else {
args.push(argToCheck)
}
}
return {
options: convertCasing ? convertToCamelCase(options) : options,
args
}
}
}
function optionsToString(options, omitHelp = false) {
const optionsToShow = Object.values(options).concat(omitHelp ? [] : {
name: 'help', shortName: 'h', description: 'Show this message and exit.'
})
const textsWithoutDescription = optionsToShow.map(option =>
` ${option.shortName ? `-${option.shortName}, ` : ''}--${option.name}`
)
const maxLength = Math.max(...(textsWithoutDescription.map(text => text.length)))
return textsWithoutDescription.map(
(text, index) => `${text.padEnd(maxLength + 5)}${optionsToShow[index].description}`
).join('\n')
}
function convertToCamelCase(options) {
return Object.fromEntries(Object.entries(options).map(
([key, value]) => [
key.replace(/-[a-z]/, match => match.at(1).toUpperCase()),
value
])
)
}
export function parseArguments(options) {
return new ArgumentParser(options)
}