// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
/**
* Command line arguments parser based on
* {@link https://github.com/minimistjs/minimist | minimist}.
*
* This module is browser compatible.
*
* @example
* ```ts
* import { parse } from "https://deno.land/std@$STD_VERSION/flags/mod.ts";
*
* console.dir(parse(Deno.args));
* ```
*
* @deprecated Use
* {@linkcode https://jsr.io/@std/cli/doc/parse-args/~/parseArgs | parseArgs}
* instead. This module will be removed once the Standard Library migrates to
* {@link https://jsr.io/ | JSR}.
*
* @module
*/
import { assertExists } from "../assert/assert_exists.ts";
/** Combines recursively all intersection types and returns a new single type. */
type Id = TRecord extends Record
? TRecord extends infer InferredRecord
? { [Key in keyof InferredRecord]: Id }
: never
: TRecord;
/** Converts a union type `A | B | C` into an intersection type `A & B & C`. */
type UnionToIntersection =
(TValue extends unknown ? (args: TValue) => unknown : never) extends
(args: infer R) => unknown ? R extends Record ? R : never
: never;
type BooleanType = boolean | string | undefined;
type StringType = string | undefined;
type ArgType = StringType | BooleanType;
type Collectable = string | undefined;
type Negatable = string | undefined;
type UseTypes<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
> = undefined extends (
& (false extends TBooleans ? undefined : TBooleans)
& TCollectable
& TStrings
) ? false
: true;
/**
* Creates a record with all available flags with the corresponding type and
* default type.
*/
type Values<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
TNegatable extends Negatable,
TDefault extends Record | undefined,
TAliases extends Aliases | undefined,
> = UseTypes extends true ?
& Record
& AddAliases<
SpreadDefaults<
& CollectValues
& RecursiveRequired>
& CollectUnknownValues<
TBooleans,
TStrings,
TCollectable,
TNegatable
>,
DedotRecord
>,
TAliases
>
// deno-lint-ignore no-explicit-any
: Record;
type Aliases = Partial<
Record, TAliasNames | ReadonlyArray>
>;
type AddAliases<
TArgs,
TAliases extends Aliases | undefined,
> = {
[TArgName in keyof TArgs as AliasNames]: TArgs[TArgName];
};
type AliasNames<
TArgName,
TAliases extends Aliases | undefined,
> = TArgName extends keyof TAliases
? string extends TAliases[TArgName] ? TArgName
: TAliases[TArgName] extends string ? TArgName | TAliases[TArgName]
: TAliases[TArgName] extends Array
? TArgName | TAliases[TArgName][number]
: TArgName
: TArgName;
/**
* Spreads all default values of Record `TDefaults` into Record `TArgs`
* and makes default values required.
*
* **Example:**
* `SpreadValues<{ foo?: boolean, bar?: number }, { foo: number }>`
*
* **Result:** `{ foo: boolean | number, bar?: number }`
*/
type SpreadDefaults = TDefaults extends undefined ? TArgs
: TArgs extends Record ?
& Omit
& {
[Default in keyof TDefaults]: Default extends keyof TArgs
? (TArgs[Default] & TDefaults[Default] | TDefaults[Default]) extends
Record
? NonNullable>
: TDefaults[Default] | NonNullable
: unknown;
}
: never;
/**
* Defines the Record for the `default` option to add
* auto-suggestion support for IDE's.
*/
type Defaults = Id<
UnionToIntersection<
& Record
// Dedotted auto suggestions: { foo: { bar: unknown } }
& MapTypes
& MapTypes
// Flat auto suggestions: { "foo.bar": unknown }
& MapDefaults
& MapDefaults
>
>;
type MapDefaults = Partial<
Record
>;
type RecursiveRequired = TRecord extends Record ? {
[Key in keyof TRecord]-?: RecursiveRequired;
}
: TRecord;
/** Same as `MapTypes` but also supports collectable options. */
type CollectValues<
TArgNames extends ArgType,
TType,
TCollectable extends Collectable,
TNegatable extends Negatable = undefined,
> = UnionToIntersection<
Extract extends string ?
& (Exclude extends never ? Record
: MapTypes, TType, TNegatable>)
& (Extract extends never ? Record
: RecursiveRequired<
MapTypes, Array, TNegatable>
>)
: MapTypes
>;
/** Same as `Record` but also supports dotted and negatable options. */
type MapTypes<
TArgNames extends ArgType,
TType,
TNegatable extends Negatable = undefined,
> = undefined extends TArgNames ? Record
: TArgNames extends `${infer Name}.${infer Rest}` ? {
[Key in Name]?: MapTypes<
Rest,
TType,
TNegatable extends `${Name}.${infer Negate}` ? Negate : undefined
>;
}
: TArgNames extends string ? Partial<
Record
>
: Record;
type CollectUnknownValues<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
TNegatable extends Negatable,
> = UnionToIntersection<
TCollectable extends TBooleans & TStrings ? Record
: DedotRecord<
// Unknown collectable & non-negatable args.
& Record<
Exclude<
Extract, string>,
Extract
>,
Array
>
// Unknown collectable & negatable args.
& Record<
Exclude<
Extract, string>,
Extract
>,
Array | false
>
>
>;
/** Converts `{ "foo.bar.baz": unknown }` into `{ foo: { bar: { baz: unknown } } }`. */
type DedotRecord = Record extends TRecord ? TRecord
: TRecord extends Record ? UnionToIntersection<
ValueOf<
{
[Key in keyof TRecord]: Key extends string ? Dedot
: never;
}
>
>
: TRecord;
type Dedot = TKey extends
`${infer Name}.${infer Rest}` ? { [Key in Name]: Dedot }
: { [Key in TKey]: TValue };
type ValueOf = TValue[keyof TValue];
/**
* The value returned from `parse`.
*
* @deprecated This will be removed in 1.0.0. Import from
* {@link https://deno.land/std/cli/parse_args.ts} instead.
*/
export type Args<
// deno-lint-ignore no-explicit-any
TArgs extends Record = Record,
TDoubleDash extends boolean | undefined = undefined,
> = Id<
& TArgs
& {
/** Contains all the arguments that didn't have an option associated with
* them. */
_: Array;
}
& (boolean extends TDoubleDash ? DoubleDash
: true extends TDoubleDash ? Required
: Record)
>;
type DoubleDash = {
/** Contains all the arguments that appear after the double dash: "--". */
"--"?: Array;
};
/**
* The options for the `parse` call.
*
* @deprecated This will be removed in 1.0.0. Import from
* {@link https://deno.land/std/cli/parse_args.ts} instead.
*/
export interface ParseOptions<
TBooleans extends BooleanType = BooleanType,
TStrings extends StringType = StringType,
TCollectable extends Collectable = Collectable,
TNegatable extends Negatable = Negatable,
TDefault extends Record | undefined =
| Record
| undefined,
TAliases extends Aliases | undefined = Aliases | undefined,
TDoubleDash extends boolean | undefined = boolean | undefined,
> {
/**
* When `true`, populate the result `_` with everything before the `--` and
* the result `['--']` with everything after the `--`.
*
* @default {false}
*
* @example
* ```ts
* // $ deno run example.ts -- a arg1
* import { parse } from "https://deno.land/std@$STD_VERSION/flags/mod.ts";
* console.dir(parse(Deno.args, { "--": false }));
* // output: { _: [ "a", "arg1" ] }
* console.dir(parse(Deno.args, { "--": true }));
* // output: { _: [], --: [ "a", "arg1" ] }
* ```
*/
"--"?: TDoubleDash;
/**
* An object mapping string names to strings or arrays of string argument
* names to use as aliases.
*/
alias?: TAliases;
/**
* A boolean, string or array of strings to always treat as booleans. If
* `true` will treat all double hyphenated arguments without equal signs as
* `boolean` (e.g. affects `--foo`, not `-f` or `--foo=bar`).
* All `boolean` arguments will be set to `false` by default.
*/
boolean?: TBooleans | ReadonlyArray>;
/** An object mapping string argument names to default values. */
default?: TDefault & Defaults;
/**
* When `true`, populate the result `_` with everything after the first
* non-option.
*/
stopEarly?: boolean;
/** A string or array of strings argument names to always treat as strings. */
string?: TStrings | ReadonlyArray>;
/**
* A string or array of strings argument names to always treat as arrays.
* Collectable options can be used multiple times. All values will be
* collected into one array. If a non-collectable option is used multiple
* times, the last value is used.
* All Collectable arguments will be set to `[]` by default.
*/
collect?: TCollectable | ReadonlyArray>;
/**
* A string or array of strings argument names which can be negated
* by prefixing them with `--no-`, like `--no-config`.
*/
negatable?: TNegatable | ReadonlyArray>;
/**
* A function which is invoked with a command line parameter not defined in
* the `options` configuration object. If the function returns `false`, the
* unknown option is not added to `parsedArgs`.
*/
unknown?: (arg: string, key?: string, value?: unknown) => unknown;
}
interface Flags {
bools: Record;
strings: Record;
collect: Record;
negatable: Record;
unknownFn: (arg: string, key?: string, value?: unknown) => unknown;
allBools: boolean;
}
interface NestedMapping {
[key: string]: NestedMapping | unknown;
}
const { hasOwn } = Object;
function get(
obj: Record,
key: string,
): TValue | undefined {
if (hasOwn(obj, key)) {
return obj[key];
}
}
function getForce(obj: Record, key: string): TValue {
const v = get(obj, key);
assertExists(v);
return v;
}
function isNumber(x: unknown): boolean {
if (typeof x === "number") return true;
if (/^0x[0-9a-f]+$/i.test(String(x))) return true;
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(String(x));
}
function hasKey(obj: NestedMapping, keys: string[]): boolean {
let o = obj;
keys.slice(0, -1).forEach((key) => {
o = (get(o, key) ?? {}) as NestedMapping;
});
const key = keys.at(-1);
return key !== undefined && hasOwn(o, key);
}
/**
* 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, the flags module tries to determine the type of all arguments
* automatically and the return type of the `parse` method 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
* the `parse` method 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.
*
* @example
* ```ts
* import { parse } from "https://deno.land/std@$STD_VERSION/flags/mod.ts";
* const parsedArgs = parse(Deno.args);
* ```
*
* @example
* ```ts
* import { parse } from "https://deno.land/std@$STD_VERSION/flags/mod.ts";
* const parsedArgs = parse(["--foo", "--bar=baz", "./quux.txt"]);
* // parsedArgs: { foo: true, bar: "baz", _: ["./quux.txt"] }
* ```
*
* @deprecated Use
* {@linkcode https://jsr.io/@std/cli/doc/parse-args/~/parseArgs | parseArgs}
* instead. This module will be removed once the Standard Library migrates to
* {@link https://jsr.io/ | JSR}.
*/
export function parse<
TArgs extends Values<
TBooleans,
TStrings,
TCollectable,
TNegatable,
TDefaults,
TAliases
>,
TDoubleDash extends boolean | undefined = undefined,
TBooleans extends BooleanType = undefined,
TStrings extends StringType = undefined,
TCollectable extends Collectable = undefined,
TNegatable extends Negatable = undefined,
TDefaults extends Record | undefined = undefined,
TAliases extends Aliases | undefined = undefined,
TAliasArgNames extends string = string,
TAliasNames extends string = string,
>(
args: string[],
{
"--": doubleDash = false,
alias = {} as NonNullable,
boolean = false,
default: defaults = {} as TDefaults & Defaults,
stopEarly = false,
string = [],
collect = [],
negatable = [],
unknown = (i: string): unknown => i,
}: ParseOptions<
TBooleans,
TStrings,
TCollectable,
TNegatable,
TDefaults,
TAliases,
TDoubleDash
> = {},
): Args {
const aliases: Record = {};
const flags: Flags = {
bools: {},
strings: {},
unknownFn: unknown,
allBools: false,
collect: {},
negatable: {},
};
if (alias !== undefined) {
for (const key in alias) {
const val = getForce(alias, key);
if (typeof val === "string") {
aliases[key] = [val];
} else {
aliases[key] = val as Array;
}
const aliasesForKey = getForce(aliases, key);
for (const alias of aliasesForKey) {
aliases[alias] = [key].concat(aliasesForKey.filter((y) => alias !== y));
}
}
}
if (boolean !== undefined) {
if (typeof boolean === "boolean") {
flags.allBools = !!boolean;
} else {
const booleanArgs: ReadonlyArray = typeof boolean === "string"
? [boolean]
: boolean;
for (const key of booleanArgs.filter(Boolean)) {
flags.bools[key] = true;
const alias = get(aliases, key);
if (alias) {
for (const al of alias) {
flags.bools[al] = true;
}
}
}
}
}
if (string !== undefined) {
const stringArgs: ReadonlyArray = typeof string === "string"
? [string]
: string;
for (const key of stringArgs.filter(Boolean)) {
flags.strings[key] = true;
const alias = get(aliases, key);
if (alias) {
for (const al of alias) {
flags.strings[al] = true;
}
}
}
}
if (collect !== undefined) {
const collectArgs: ReadonlyArray = typeof collect === "string"
? [collect]
: collect;
for (const key of collectArgs.filter(Boolean)) {
flags.collect[key] = true;
const alias = get(aliases, key);
if (alias) {
for (const al of alias) {
flags.collect[al] = true;
}
}
}
}
if (negatable !== undefined) {
const negatableArgs: ReadonlyArray = typeof negatable === "string"
? [negatable]
: negatable;
for (const key of negatableArgs.filter(Boolean)) {
flags.negatable[key] = true;
const alias = get(aliases, key);
if (alias) {
for (const al of alias) {
flags.negatable[al] = true;
}
}
}
}
const argv: Args = { _: [] };
function argDefined(key: string, arg: string): boolean {
return (
(flags.allBools && /^--[^=]+$/.test(arg)) ||
get(flags.bools, key) ||
!!get(flags.strings, key) ||
!!get(aliases, key)
);
}
function setKey(
obj: NestedMapping,
name: string,
value: unknown,
collect = true,
) {
let o = obj;
const keys = name.split(".");
keys.slice(0, -1).forEach(function (key) {
if (get(o, key) === undefined) {
o[key] = {};
}
o = get(o, key) as NestedMapping;
});
const key = keys.at(-1)!;
const collectable = collect && !!get(flags.collect, name);
if (!collectable) {
o[key] = value;
} else if (get(o, key) === undefined) {
o[key] = [value];
} else if (Array.isArray(get(o, key))) {
(o[key] as unknown[]).push(value);
} else {
o[key] = [get(o, key), value];
}
}
function setArg(
key: string,
val: unknown,
arg: string | undefined = undefined,
collect?: boolean,
) {
if (arg && flags.unknownFn && !argDefined(key, arg)) {
if (flags.unknownFn(arg, key, val) === false) return;
}
const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val;
setKey(argv, key, value, collect);
const alias = get(aliases, key);
if (alias) {
for (const x of alias) {
setKey(argv, x, value, collect);
}
}
}
function aliasIsBoolean(key: string): boolean {
return getForce(aliases, key).some(
(x) => typeof get(flags.bools, x) === "boolean",
);
}
let notFlags: string[] = [];
// all args after "--" are not parsed
if (args.includes("--")) {
notFlags = args.slice(args.indexOf("--") + 1);
args = args.slice(0, args.indexOf("--"));
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
assertExists(arg);
if (/^--.+=/.test(arg)) {
const m = arg.match(/^--([^=]+)=(.*)$/s);
assertExists(m);
const [, key, value] = m;
assertExists(key);
if (flags.bools[key]) {
const booleanValue = value !== "false";
setArg(key, booleanValue, arg);
} else {
setArg(key, value, arg);
}
} else if (
/^--no-.+/.test(arg) && get(flags.negatable, arg.replace(/^--no-/, ""))
) {
const m = arg.match(/^--no-(.+)/);
assertExists(m);
assertExists(m[1]);
setArg(m[1], false, arg, false);
} else if (/^--.+/.test(arg)) {
const m = arg.match(/^--(.+)/);
assertExists(m);
assertExists(m[1]);
const [, key] = m;
const next = args[i + 1];
if (
next !== undefined &&
!/^-/.test(next) &&
!get(flags.bools, key) &&
!flags.allBools &&
(get(aliases, key) ? !aliasIsBoolean(key) : true)
) {
setArg(key, next, arg);
i++;
} else if (next !== undefined && (next === "true" || next === "false")) {
setArg(key, next === "true", arg);
i++;
} else {
setArg(key, get(flags.strings, key) ? "" : true, arg);
}
} else if (/^-[^-]+/.test(arg)) {
const letters = arg.slice(1, -1).split("");
let broken = false;
for (const [j, letter] of letters.entries()) {
const next = arg.slice(j + 2);
if (next === "-") {
setArg(letter, next, arg);
continue;
}
if (/[A-Za-z]/.test(letter) && next.includes("=")) {
setArg(letter, next.split(/=(.+)/)[1], arg);
broken = true;
break;
}
if (
/[A-Za-z]/.test(letter) &&
/-?\d+(\.\d*)?(e-?\d+)?$/.test(next)
) {
setArg(letter, next, arg);
broken = true;
break;
}
if (letters[j + 1]?.match(/\W/)) {
setArg(letter, arg.slice(j + 2), arg);
broken = true;
break;
} else {
setArg(letter, get(flags.strings, letter) ? "" : true, arg);
}
}
const key = arg.at(-1)!;
if (!broken && key !== "-") {
const nextArg = args[i + 1];
if (
nextArg &&
!/^(-|--)[^-]/.test(nextArg) &&
!get(flags.bools, key) &&
(get(aliases, key) ? !aliasIsBoolean(key) : true)
) {
setArg(key, nextArg, arg);
i++;
} else if (nextArg && (nextArg === "true" || nextArg === "false")) {
setArg(key, nextArg === "true", arg);
i++;
} else {
setArg(key, get(flags.strings, key) ? "" : true, arg);
}
}
} else {
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings["_"] ?? !isNumber(arg) ? arg : Number(arg));
}
if (stopEarly) {
argv._.push(...args.slice(i + 1));
break;
}
}
}
for (const [key, value] of Object.entries(defaults)) {
if (!hasKey(argv, key.split("."))) {
setKey(argv, key, value, false);
const alias = aliases[key];
if (alias !== undefined) {
for (const x of alias) {
setKey(argv, x, value, false);
}
}
}
}
for (const key of Object.keys(flags.bools)) {
if (!hasKey(argv, key.split("."))) {
const value = get(flags.collect, key) ? [] : false;
setKey(
argv,
key,
value,
false,
);
}
}
for (const key of Object.keys(flags.strings)) {
if (!hasKey(argv, key.split(".")) && get(flags.collect, key)) {
setKey(
argv,
key,
[],
false,
);
}
}
if (doubleDash) {
argv["--"] = [];
for (const key of notFlags) {
argv["--"].push(key);
}
} else {
for (const key of notFlags) {
argv._.push(key);
}
}
return argv as Args;
}