@lonu/stc
Version:
A tool for converting OpenApi/Swagger/Apifox into code.
358 lines (357 loc) • 12.9 kB
JavaScript
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.
const FLAG_REGEXP = /^(?:-(?:(?<doubleDash>-)(?<negated>no-)?)?)(?<key>.+?)(?:=(?<value>.+?))?$/s;
const LETTER_REGEXP = /[A-Za-z]/;
const NUMBER_REGEXP = /-?\d+(\.\d*)?(e-?\d+)?$/;
const HYPHEN_REGEXP = /^(-|--)[^-]/;
const VALUE_REGEXP = /=(?<value>.+)/;
const FLAG_NAME_REGEXP = /^--[^=]+$/;
const SPECIAL_CHAR_REGEXP = /\W/;
const NON_WHITESPACE_REGEXP = /\S/;
function isNumber(string) {
return NON_WHITESPACE_REGEXP.test(string) && Number.isFinite(Number(string));
}
function setNested(object, keys, value, collect = false) {
keys = [...keys];
const key = keys.pop();
keys.forEach((key) => object = (object[key] ??= {}));
if (collect) {
const v = object[key];
if (Array.isArray(v)) {
v.push(value);
return;
}
value = v ? [v, value] : [value];
}
object[key] = value;
}
function hasNested(object, keys) {
for (const key of keys) {
const value = object[key];
if (!Object.hasOwn(object, key))
return false;
object = value;
}
return true;
}
function aliasIsBoolean(aliasMap, booleanSet, key) {
const set = aliasMap.get(key);
if (set === undefined)
return false;
for (const alias of set)
if (booleanSet.has(alias))
return true;
return false;
}
function isBooleanString(value) {
return value === "true" || value === "false";
}
function parseBooleanString(value) {
return value !== "false";
}
/**
* Take a set of command line arguments, optionally with a set of options, and
* return an object representing the flags found in the passed arguments.
*
* By default, any arguments starting with `-` or `--` are considered boolean
* flags. If the argument name is followed by an equal sign (`=`) it is
* considered a key-value pair. Any arguments which could not be parsed are
* available in the `_` property of the returned object.
*
* By default, this module tries to determine the type of all arguments
* automatically and the return type of this function will have an index
* signature with `any` as value (`{ [x: string]: any }`).
*
* If the `string`, `boolean` or `collect` option is set, the return value of
* this function will be fully typed and the index signature of the return
* type will change to `{ [x: string]: unknown }`.
*
* Any arguments after `'--'` will not be parsed and will end up in `parsedArgs._`.
*
* Numeric-looking arguments will be returned as numbers unless `options.string`
* or `options.boolean` is set for that argument name.
*
* See {@linkcode ParseOptions} for more information.
*
* @param args An array of command line arguments.
* @param options Options for the parse function.
*
* @typeParam TArgs Type of result.
* @typeParam TDoubleDash Used by `TArgs` for the result.
* @typeParam TBooleans Used by `TArgs` for the result.
* @typeParam TStrings Used by `TArgs` for the result.
* @typeParam TCollectable Used by `TArgs` for the result.
* @typeParam TNegatable Used by `TArgs` for the result.
* @typeParam TDefaults Used by `TArgs` for the result.
* @typeParam TAliases Used by `TArgs` for the result.
* @typeParam TAliasArgNames Used by `TArgs` for the result.
* @typeParam TAliasNames Used by `TArgs` for the result.
*
* @return The parsed arguments.
*
* @example Usage
* ```ts
* import { parseArgs } from "@std/cli/parse-args";
* import { assertEquals } from "@std/assert/equals";
*
* // For proper use, one should use `parseArgs(Deno.args)`
* assertEquals(parseArgs(["--foo", "--bar=baz", "./quux.txt"]), {
* foo: true,
* bar: "baz",
* _: ["./quux.txt"],
* });
* ```
*
* @example `string` and `boolean` options
*
* Use `string` and `boolean` options to specify the type of the argument.
*
* ```ts
* import { parseArgs } from "@std/cli/parse-args";
* import { assertEquals } from "@std/assert/equals";
*
* const args = parseArgs(["--foo", "--bar", "baz"], {
* boolean: ["foo"],
* string: ["bar"],
* });
*
* assertEquals(args, { foo: true, bar: "baz", _: [] });
* ```
*
* @example `collect` option
*
* `collect` option tells the parser to treat the option as an array. All
* values will be collected into one array. If a non-collectable option is used
* multiple times, the last value is used.
*
* ```ts
* import { parseArgs } from "@std/cli/parse-args";
* import { assertEquals } from "@std/assert/equals";
*
* const args = parseArgs(["--foo", "bar", "--foo", "baz"], {
* collect: ["foo"],
* });
*
* assertEquals(args, { foo: ["bar", "baz"], _: [] });
* ```
*
* @example `negatable` option
*
* `negatable` option tells the parser to treat the option can be negated by
* prefixing them with `--no-`, like `--no-config`.
*
* ```ts
* import { parseArgs } from "@std/cli/parse-args";
* import { assertEquals } from "@std/assert/equals";
*
* const args = parseArgs(["--no-foo"], {
* boolean: ["foo"],
* negatable: ["foo"],
* });
*
* assertEquals(args, { foo: false, _: [] });
* ```
*/
export function parseArgs(args, options) {
const { "--": doubleDash = false, alias = {}, boolean = false, default: defaults = {}, stopEarly = false, string = [], collect = [], negatable = [], unknown: unknownFn = (i) => i, } = options ?? {};
const aliasMap = new Map();
const booleanSet = new Set();
const stringSet = new Set();
const collectSet = new Set();
const negatableSet = new Set();
let allBools = false;
if (alias) {
for (const [key, value] of Object.entries(alias)) {
if (value === undefined) {
throw new TypeError("Alias value must be defined");
}
const aliases = Array.isArray(value) ? value : [value];
aliasMap.set(key, new Set(aliases));
aliases.forEach((alias) => aliasMap.set(alias, new Set([key, ...aliases.filter((it) => it !== alias)])));
}
}
if (boolean) {
if (typeof boolean === "boolean") {
allBools = boolean;
}
else {
const booleanArgs = Array.isArray(boolean) ? boolean : [boolean];
for (const key of booleanArgs.filter(Boolean)) {
booleanSet.add(key);
aliasMap.get(key)?.forEach((al) => {
booleanSet.add(al);
});
}
}
}
if (string) {
const stringArgs = Array.isArray(string) ? string : [string];
for (const key of stringArgs.filter(Boolean)) {
stringSet.add(key);
aliasMap.get(key)?.forEach((al) => stringSet.add(al));
}
}
if (collect) {
const collectArgs = Array.isArray(collect) ? collect : [collect];
for (const key of collectArgs.filter(Boolean)) {
collectSet.add(key);
aliasMap.get(key)?.forEach((al) => collectSet.add(al));
}
}
if (negatable) {
const negatableArgs = Array.isArray(negatable) ? negatable : [negatable];
for (const key of negatableArgs.filter(Boolean)) {
negatableSet.add(key);
aliasMap.get(key)?.forEach((alias) => negatableSet.add(alias));
}
}
const argv = { _: [] };
function setArgument(key, value, arg, collect) {
if (!booleanSet.has(key) &&
!stringSet.has(key) &&
!aliasMap.has(key) &&
!(allBools && FLAG_NAME_REGEXP.test(arg)) &&
unknownFn?.(arg, key, value) === false) {
return;
}
if (typeof value === "string" && !stringSet.has(key)) {
value = isNumber(value) ? Number(value) : value;
}
const collectable = collect && collectSet.has(key);
setNested(argv, key.split("."), value, collectable);
aliasMap.get(key)?.forEach((key) => {
setNested(argv, key.split("."), value, collectable);
});
}
let notFlags = [];
// all args after "--" are not parsed
const index = args.indexOf("--");
if (index !== -1) {
notFlags = args.slice(index + 1);
args = args.slice(0, index);
}
argsLoop: for (let i = 0; i < args.length; i++) {
const arg = args[i];
const groups = arg.match(FLAG_REGEXP)?.groups;
if (groups) {
const { doubleDash, negated } = groups;
let key = groups.key;
let value = groups.value;
if (doubleDash) {
if (value) {
if (booleanSet.has(key))
value = parseBooleanString(value);
setArgument(key, value, arg, true);
continue;
}
if (negated) {
if (negatableSet.has(key)) {
setArgument(key, false, arg, false);
continue;
}
key = `no-${key}`;
}
const next = args[i + 1];
if (next) {
if (!booleanSet.has(key) &&
!allBools &&
!next.startsWith("-") &&
(!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) {
value = next;
i++;
setArgument(key, value, arg, true);
continue;
}
if (isBooleanString(next)) {
value = parseBooleanString(next);
i++;
setArgument(key, value, arg, true);
continue;
}
}
value = stringSet.has(key) ? "" : true;
setArgument(key, value, arg, true);
continue;
}
const letters = arg.slice(1, -1).split("");
for (const [j, letter] of letters.entries()) {
const next = arg.slice(j + 2);
if (next === "-") {
setArgument(letter, next, arg, true);
continue;
}
if (LETTER_REGEXP.test(letter)) {
const groups = VALUE_REGEXP.exec(next)?.groups;
if (groups) {
setArgument(letter, groups.value, arg, true);
continue argsLoop;
}
if (NUMBER_REGEXP.test(next)) {
setArgument(letter, next, arg, true);
continue argsLoop;
}
}
if (letters[j + 1]?.match(SPECIAL_CHAR_REGEXP)) {
setArgument(letter, arg.slice(j + 2), arg, true);
continue argsLoop;
}
setArgument(letter, stringSet.has(letter) ? "" : true, arg, true);
}
key = arg.slice(-1);
if (key === "-")
continue;
const nextArg = args[i + 1];
if (nextArg) {
if (!HYPHEN_REGEXP.test(nextArg) &&
!booleanSet.has(key) &&
(!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) {
setArgument(key, nextArg, arg, true);
i++;
continue;
}
if (isBooleanString(nextArg)) {
const value = parseBooleanString(nextArg);
setArgument(key, value, arg, true);
i++;
continue;
}
}
setArgument(key, stringSet.has(key) ? "" : true, arg, true);
continue;
}
if (unknownFn?.(arg) !== false) {
argv._.push(stringSet.has("_") || !isNumber(arg) ? arg : Number(arg));
}
if (stopEarly) {
argv._.push(...args.slice(i + 1));
break;
}
}
for (const [key, value] of Object.entries(defaults)) {
const keys = key.split(".");
if (!hasNested(argv, keys)) {
setNested(argv, keys, value);
aliasMap.get(key)?.forEach((key) => setNested(argv, key.split("."), value));
}
}
for (const key of booleanSet.keys()) {
const keys = key.split(".");
if (!hasNested(argv, keys)) {
const value = collectSet.has(key) ? [] : false;
setNested(argv, keys, value);
}
}
for (const key of stringSet.keys()) {
const keys = key.split(".");
if (!hasNested(argv, keys) && collectSet.has(key)) {
setNested(argv, keys, []);
}
}
if (doubleDash) {
argv["--"] = notFlags;
}
else {
argv._.push(...notFlags);
}
return argv;
}