// util.parseArgs: Node's lib/internal/util/parse_args algorithm (reimplemented from its // documented behavior), tokenizing argv into options/positionals per the Open Group // Utility Conventions (short options, grouped shorts, `--` as a terminator). import { inspect } from "../inspect"; type OptionType = "string" | "boolean"; export interface ParseArgsOptionConfig { type: OptionType; short?: string; multiple?: boolean; default?: string | boolean | string[] | boolean[]; } export interface ParseArgsConfig { args?: string[]; options?: Record; strict?: boolean; allowPositionals?: boolean; tokens?: boolean; allowNegative?: boolean; } interface OptionToken { kind: "option"; name: string; rawName: string; index: number; value: string | undefined; inlineValue: boolean | undefined; } interface PositionalToken { kind: "positional"; index: number; value: string; } interface TerminatorToken { kind: "option-terminator"; index: number; } type Token = OptionToken | PositionalToken | TerminatorToken; export interface ParsedArgs { values: Record; positionals: string[]; tokens?: Token[]; } function argValue(value: unknown): string { let shown = inspect(value); if (shown.length > 128) shown = `${shown.slice(0, 128)}...`; return shown; } function invalidArgType(name: string, expected: string, value: unknown): TypeError { const kind = name.includes(".") ? "property" : "argument"; const got = value === null || value === undefined ? ` Received ${value}` : typeof value === "function" ? ` Received function ${(value as { name?: string }).name || "anonymous"}` : typeof value === "object" ? ` Received an instance of ${(value as object).constructor?.name ?? "Object"}` : ` Received type ${typeof value} (${typeof value === "string" ? `'${value}'` : String(value)})`; return Object.assign(new TypeError(`The "${name}" ${kind} must be ${expected}.${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } function invalidArgValue(name: string, value: unknown, reason: string): TypeError { return Object.assign(new TypeError(`The ${name.includes(".") ? "property" : "argument"} '${name}' ${reason}. Received ${argValue(value)}`), { code: "ERR_INVALID_ARG_VALUE", }); } function validateBoolean(value: unknown, name: string): void { if (typeof value !== "boolean") throw invalidArgType(name, "of type boolean", value); } function validateArray(value: unknown, name: string): void { if (!Array.isArray(value)) throw invalidArgType(name, "an instance of Array", value); } function validateObject(value: unknown, name: string): void { if (value === null || typeof value !== "object") throw invalidArgType(name, "of type object", value); } function validateString(value: unknown, name: string): void { if (typeof value !== "string") throw invalidArgType(name, "of type string", value); } function validateStringArray(value: unknown, name: string): void { validateArray(value, name); (value as unknown[]).forEach((v, i) => validateString(v, `${name}[${i}]`)); } function validateBooleanArray(value: unknown, name: string): void { validateArray(value, name); (value as unknown[]).forEach((v, i) => validateBoolean(v, `${name}[${i}]`)); } function validateUnion(value: unknown, name: string, union: readonly string[]): void { if (!union.includes(value as string)) throw invalidArgType(name, `('${union.join("|")}')`, value); } function unknownOption(option: string, allowPositionals: boolean): TypeError { const suggest = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : ""; return Object.assign(new TypeError(`Unknown option '${option}'${suggest}`), { code: "ERR_PARSE_ARGS_UNKNOWN_OPTION" }); } function unexpectedPositional(value: string): TypeError { return Object.assign( new TypeError(`Unexpected argument '${value}'. This command does not take positional arguments`), { code: "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL" }, ); } function invalidOptionValue(message: string): TypeError { return Object.assign(new TypeError(message), { code: "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" }); } function optionsGetOwn(options: Record, longOption: string, prop: "type" | "short" | "multiple"): unknown { return Object.hasOwn(options, longOption) ? options[longOption]![prop] : undefined; } /** Open Group Utility Conventions: anything after the option may be its value, even if * it starts with a dash — greedy, like Node. */ function isOptionValue(value: string | undefined): boolean { return value !== undefined && value !== null; } function isOptionLikeValue(value: string | undefined): boolean { return value !== undefined && value.length > 1 && value[0] === "-"; } function isLoneShortOption(arg: string): boolean { return arg.length === 2 && arg[0] === "-" && arg[1] !== "-"; } function isLoneLongOption(arg: string): boolean { return arg.length > 2 && arg.startsWith("--") && arg.indexOf("=", 3) === -1; } function isLongOptionAndValue(arg: string): boolean { return arg.length > 2 && arg.startsWith("--") && arg.indexOf("=", 3) !== -1; } function findLongOptionForShort(shortOption: string, options: Record): string { for (const [long, config] of Object.entries(options)) { if (config.short === shortOption) return long; } return shortOption; } function isShortOptionGroup(arg: string, options: Record): boolean { if (arg.length <= 2 || arg[0] !== "-" || arg[1] === "-") return false; const longOption = findLongOptionForShort(arg[1]!, options); return optionsGetOwn(options, longOption, "type") !== "string"; } function isShortOptionAndValue(arg: string, options: Record): boolean { if (arg.length <= 2 || arg[0] !== "-" || arg[1] === "-") return false; const longOption = findLongOptionForShort(arg[1]!, options); return optionsGetOwn(options, longOption, "type") === "string"; } function argsToTokens(args: string[], options: Record): Token[] { const tokens: Token[] = []; let index = -1; let groupCount = 0; const remaining = [...args]; while (remaining.length > 0) { const arg = remaining.shift()!; const nextArg = remaining[0]; if (groupCount > 0) groupCount--; else index++; if (arg === "--") { tokens.push({ kind: "option-terminator", index }); for (const value of remaining) tokens.push({ kind: "positional", index: ++index, value }); break; } if (isLoneShortOption(arg)) { const shortOption = arg[1]!; const longOption = findLongOptionForShort(shortOption, options); let value: string | undefined; let inlineValue: boolean | undefined; if (optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg)) { value = remaining.shift(); inlineValue = false; } tokens.push({ kind: "option", name: longOption, rawName: arg, index, value, inlineValue }); if (value != null) ++index; continue; } if (isShortOptionGroup(arg, options)) { const expanded: string[] = []; for (let i = 1; i < arg.length; i++) { const shortOption = arg[i]!; const longOption = findLongOptionForShort(shortOption, options); if (optionsGetOwn(options, longOption, "type") !== "string" || i === arg.length - 1) { expanded.push(`-${shortOption}`); } else { expanded.push(`-${arg.slice(i)}`); break; } } remaining.unshift(...expanded); groupCount = expanded.length; continue; } if (isShortOptionAndValue(arg, options)) { const shortOption = arg[1]!; const longOption = findLongOptionForShort(shortOption, options); tokens.push({ kind: "option", name: longOption, rawName: `-${shortOption}`, index, value: arg.slice(2), inlineValue: true }); continue; } if (isLoneLongOption(arg)) { const longOption = arg.slice(2); let value: string | undefined; let inlineValue: boolean | undefined; if (optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg)) { value = remaining.shift(); inlineValue = false; } tokens.push({ kind: "option", name: longOption, rawName: arg, index, value, inlineValue }); if (value != null) ++index; continue; } if (isLongOptionAndValue(arg)) { const equalIndex = arg.indexOf("="); const longOption = arg.slice(2, equalIndex); tokens.push({ kind: "option", name: longOption, rawName: `--${longOption}`, index, value: arg.slice(equalIndex + 1), inlineValue: true }); continue; } tokens.push({ kind: "positional", index, value: arg }); } return tokens; } function checkOptionLikeValue(token: OptionToken): void { if (!token.inlineValue && isOptionLikeValue(token.value)) { const example = token.rawName.startsWith("--") ? `'${token.rawName}=-XYZ'` : `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; throw invalidOptionValue(`Option '${token.rawName}' argument is ambiguous.\n` + `Did you forget to specify the option argument for '${token.rawName}'?\n` + `To specify an option argument starting with a dash use ${example}.`); } } function checkOptionUsage(config: Required>, token: OptionToken): void { let tokenName = token.name; if (!Object.hasOwn(config.options, tokenName)) { if (config.allowNegative && tokenName.startsWith("no-")) { tokenName = tokenName.slice(3); if (!Object.hasOwn(config.options, tokenName) || optionsGetOwn(config.options, tokenName, "type") !== "boolean") { throw unknownOption(token.rawName, config.allowPositionals); } } else { throw unknownOption(token.rawName, config.allowPositionals); } } const short = optionsGetOwn(config.options, tokenName, "short") as string | undefined; const shortAndLong = `${short ? `-${short}, ` : ""}--${tokenName}`; const type = optionsGetOwn(config.options, tokenName, "type"); if (type === "string" && typeof token.value !== "string") { throw invalidOptionValue(`Option '${shortAndLong} ' argument missing`); } if (type === "boolean" && token.value != null) { throw invalidOptionValue(`Option '${shortAndLong}' does not take an argument`); } } function storeOption( token: OptionToken, options: Record, values: ParsedArgs["values"], allowNegative: boolean, ): void { let longOption = token.name; if (longOption === "__proto__") return; let optionValue: string | boolean | undefined = token.value; if (allowNegative && longOption.startsWith("no-") && optionValue === undefined) { longOption = longOption.slice(3); optionValue = false; } const newValue = optionValue ?? true; if (optionsGetOwn(options, longOption, "multiple")) { const existing = values[longOption]; if (Array.isArray(existing)) (existing as unknown[]).push(newValue); else values[longOption] = [newValue] as string[] | boolean[]; } else { values[longOption] = newValue; } } export function parseArgs(config: ParseArgsConfig | undefined, mainArgs: () => string[]): ParsedArgs { const cfg = config ?? {}; const args = cfg.args ?? mainArgs(); const strict = cfg.strict ?? true; const allowPositionals = cfg.allowPositionals ?? !strict; const returnTokens = cfg.tokens ?? false; const allowNegative = cfg.allowNegative ?? false; const options = cfg.options ?? {}; validateArray(args, "args"); validateBoolean(strict, "strict"); validateBoolean(allowPositionals, "allowPositionals"); validateBoolean(returnTokens, "tokens"); validateBoolean(allowNegative, "allowNegative"); validateObject(options, "options"); for (const [longOption, optionConfig] of Object.entries(options)) { validateObject(optionConfig, `options.${longOption}`); const optionType = optionConfig.type; validateUnion(optionType, `options.${longOption}.type`, ["string", "boolean"]); if (Object.hasOwn(optionConfig, "short")) { validateString(optionConfig.short, `options.${longOption}.short`); if (optionConfig.short!.length !== 1) throw invalidArgValue(`options.${longOption}.short`, optionConfig.short, "must be a single character"); } const multiple = optionConfig.multiple; if (Object.hasOwn(optionConfig, "multiple")) validateBoolean(multiple, `options.${longOption}.multiple`); const def = optionConfig.default; if (def !== undefined) { const validator = optionType === "string" ? (multiple ? validateStringArray : validateString) : (multiple ? validateBooleanArray : validateBoolean); validator(def, `options.${longOption}.default`); } } const tokens = argsToTokens(args, options); const result: ParsedArgs = { values: Object.create(null), positionals: [] }; if (returnTokens) result.tokens = tokens; for (const token of tokens) { if (token.kind === "option") { if (strict) { checkOptionUsage({ options, allowPositionals, allowNegative }, token); checkOptionLikeValue(token); } storeOption(token, options, result.values, allowNegative); } else if (token.kind === "positional") { if (!allowPositionals) throw unexpectedPositional(token.value); result.positionals.push(token.value); } } for (const [longOption, optionConfig] of Object.entries(options)) { if (optionConfig.default !== undefined && result.values[longOption] === undefined) { result.values[longOption] = optionConfig.default; } } return result; }