UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

160 lines (134 loc) 4.3 kB
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) }