UNPKG

args-tokens

Version:

parseArgs tokens compatibility and more high-performance parser

561 lines (559 loc) 17.4 kB
import { ArgToken } from "./parser-C6MbpZjd.js"; //#region src/resolver.d.ts /** * An argument schema definition for command-line argument parsing. * * This schema is similar to the schema of Node.js `util.parseArgs` but with extended features: * - Additional `required` and `description` properties * - Extended `type` support: 'string', 'boolean', 'number', 'enum', 'positional', 'custom' * - Simplified `default` property (single type, not union types) * * @example * Basic string argument: * ```ts * const schema: ArgSchema = { * type: 'string', * description: 'Server hostname', * default: 'localhost' * } * ``` * * @example * Required number argument with alias: * ```ts * const schema: ArgSchema = { * type: 'number', * short: 'p', * description: 'Port number to listen on', * required: true * } * ``` * * @example * Enum argument with choices: * ```ts * const schema: ArgSchema = { * type: 'enum', * choices: ['info', 'warn', 'error'], * description: 'Logging level', * default: 'info' * } * ``` */ interface ArgSchema { /** * Type of the argument value. * * - `'string'`: Text value (default if not specified) * - `'boolean'`: `true`/`false` flag (can be negatable with `--no-` prefix) * - `'number'`: Numeric value (parsed as integer or float) * - `'enum'`: One of predefined string values (requires `choices` property) * - `'positional'`: Non-option argument by position * - `'custom'`: Custom parsing with user-defined `parse` function * * @example * Different argument types: * ```ts * { * name: { type: 'string' }, // --name value * verbose: { type: 'boolean' }, // --verbose or --no-verbose * port: { type: 'number' }, // --port 3000 * level: { type: 'enum', choices: ['debug', 'info'] }, * file: { type: 'positional' }, // first positional arg * config: { type: 'custom', parse: JSON.parse } * } * ``` */ type: 'string' | 'boolean' | 'number' | 'enum' | 'positional' | 'custom'; /** * Single character alias for the long option name. * * As example, allows users to use `-x` instead of `--extended-option`. * Only valid for non-positional argument types. * * @example * Short alias usage: * ```ts * { * verbose: { * type: 'boolean', * short: 'v' // Enables both --verbose and -v * }, * port: { * type: 'number', * short: 'p' // Enables both --port 3000 and -p 3000 * } * } * ``` */ short?: string; /** * Human-readable description of the argument's purpose. * * Used for help text generation and documentation. * Should be concise but descriptive enough to understand the argument's role. * * @example * Descriptive help text: * ```ts * { * config: { * type: 'string', * description: 'Path to configuration file' * }, * timeout: { * type: 'number', * description: 'Request timeout in milliseconds' * } * } * ``` */ description?: string; /** * Marks the argument as required. * * When `true`, the argument must be provided by the user. * If missing, an `ArgResolveError` with type 'required' will be thrown. * * Note: Only `true` is allowed (not `false`) to make intent explicit. * * @example * Required arguments: * ```ts * { * input: { * type: 'string', * required: true, // Must be provided: --input file.txt * description: 'Input file path' * }, * source: { * type: 'positional', * required: true // First positional argument must exist * } * } * ``` */ required?: true; /** * Allows the argument to accept multiple values. * * When `true`, the resolved value becomes an array. * For options: can be specified multiple times (--tag foo --tag bar) * For positional: collects remaining positional arguments * * Note: Only `true` is allowed (not `false`) to make intent explicit. * * @example * Multiple values: * ```ts * { * tags: { * type: 'string', * multiple: true, // --tags foo --tags bar → ['foo', 'bar'] * description: 'Tags to apply' * }, * files: { * type: 'positional', * multiple: true // Collects all remaining positional args * } * } * ``` */ multiple?: true; /** * Enables negation for boolean arguments using `--no-` prefix. * * When `true`, allows users to explicitly set the boolean to `false` * using `--no-option-name`. When `false` or omitted, only positive * form is available. * * Only applicable to `type: 'boolean'` arguments. * * @example * Negatable boolean: * ```ts * { * color: { * type: 'boolean', * negatable: true, * default: true, * description: 'Enable colorized output' * } * // Usage: --color (true), --no-color (false) * } * ``` */ negatable?: boolean; /** * Array of allowed string values for enum-type arguments. * * Required when `type: 'enum'`. The argument value must be one of these choices, * otherwise an `ArgResolveError` with type 'type' will be thrown. * * Supports both mutable arrays and readonly arrays for type safety. * * @example * Enum choices: * ```ts * { * logLevel: { * type: 'enum', * choices: ['debug', 'info', 'warn', 'error'] as const, * default: 'info', * description: 'Logging verbosity level' * }, * format: { * type: 'enum', * choices: ['json', 'yaml', 'toml'], * description: 'Output format' * } * } * ``` */ choices?: string[] | readonly string[]; /** * Default value used when the argument is not provided. * * The type must match the argument's `type` property: * - `string` type: string default * - `boolean` type: boolean default * - `number` type: number default * - `enum` type: must be one of the `choices` values * - `positional`/`custom` type: any appropriate default * * @example * Default values by type: * ```ts * { * host: { * type: 'string', * default: 'localhost' // string default * }, * verbose: { * type: 'boolean', * default: false // boolean default * }, * port: { * type: 'number', * default: 8080 // number default * }, * level: { * type: 'enum', * choices: ['low', 'high'], * default: 'low' // must be in choices * } * } * ``` */ default?: string | boolean | number; /** * Converts the argument name from camelCase to kebab-case for CLI usage. * * When `true`, a property like `maxCount` becomes available as `--max-count`. * This allows [CAC](https://github.com/cacjs/cac) user-friendly property names while maintaining CLI conventions. * * Can be overridden globally with `resolveArgs({ toKebab: true })`. * * Note: Only `true` is allowed (not `false`) to make intent explicit. * * @example * Kebab-case conversion: * ```ts * { * maxRetries: { * type: 'number', * toKebab: true, // Accessible as --max-retries * description: 'Maximum retry attempts' * }, * enableLogging: { * type: 'boolean', * toKebab: true // Accessible as --enable-logging * } * } * ``` */ toKebab?: true; /** * Names of other options that conflict with this option. * * When this option is used together with any of the conflicting options, * an `ArgResolveError` with type 'conflict' will be thrown. * * Conflicts only need to be defined on one side - if option A defines a conflict * with option B, the conflict is automatically detected when both are used, * regardless of whether B also defines a conflict with A. * * Supports both single option name or array of option names. * Option names must match the property keys in the schema object exactly * (no automatic conversion between camelCase and kebab-case). * * @example * Single conflict (bidirectional definition): * ```ts * { * summer: { * type: 'boolean', * conflicts: 'autumn' // Cannot use --summer with --autumn * }, * autumn: { * type: 'boolean', * conflicts: 'summer' // Can define on both sides for clarity * } * } * ``` * * @example * Single conflict (one-way definition): * ```ts * { * summer: { * type: 'boolean', * conflicts: 'autumn' // Only defined on summer side * }, * autumn: { * type: 'boolean' * // No conflicts defined, but still cannot use with --summer * } * } * // Usage: --summer --autumn will throw error * // Error: "Optional argument '--summer' conflicts with '--autumn'" * ``` * * @example * Multiple conflicts: * ```ts * { * port: { * type: 'number', * conflicts: ['socket', 'pipe'], // Cannot use with --socket or --pipe * description: 'TCP port number' * }, * socket: { * type: 'string', * conflicts: ['port', 'pipe'], // Cannot use with --port or --pipe * description: 'Unix socket path' * }, * pipe: { * type: 'string', * conflicts: ['port', 'socket'], // Cannot use with --port or --socket * description: 'Named pipe path' * } * } * // These three options are mutually exclusive * ``` * * @example * With kebab-case conversion: * ```ts * { * summerSeason: { * type: 'boolean', * toKebab: true, // Accessible as --summer-season * conflicts: 'autumnSeason' // Must use property key, not CLI name * }, * autumnSeason: { * type: 'boolean', * toKebab: true // Accessible as --autumn-season * } * } * // Error: "Optional argument '--summer-season' conflicts with '--autumn-season'" * ``` */ conflicts?: string | string[]; /** * Custom parsing function for `type: 'custom'` arguments. * * Required when `type: 'custom'`. Receives the raw string value and must * return the parsed result. Should throw an Error (or subclass) if parsing fails. * * The function's return type becomes the resolved argument type. * * @param value - Raw string value from command line * @returns Parsed value of any type * @throws Error or subclass when value is invalid * * @example * Custom parsing functions: * ```ts * { * config: { * type: 'custom', * parse: (value: string) => { * try { * return JSON.parse(value) // Parse JSON config * } catch { * throw new Error('Invalid JSON configuration') * } * }, * description: 'JSON configuration object' * }, * date: { * type: 'custom', * parse: (value: string) => { * const date = new Date(value) * if (isNaN(date.getTime())) { * throw new Error('Invalid date format') * } * return date * } * } * } * ``` */ parse?: (value: string) => any; } /** * An object that contains {@link ArgSchema | argument schema}. * * This type is used to define the structure and validation rules for command line arguments. */ interface Args { [option: string]: ArgSchema; } /** * An object that contains the values of the arguments. * * @typeParam T - {@link Args | Arguments} which is an object that defines the command line arguments. */ type ArgValues<T> = T extends Args ? ResolveArgValues<T, { [Arg in keyof T]: ExtractOptionValue<T[Arg]> }> : { [option: string]: string | boolean | number | (string | boolean | number)[] | undefined; }; type IsFunction<T> = T extends ((...args: any[]) => any) ? true : false; /** * Extracts the value type from the argument schema. * * @typeParam A - {@link ArgSchema | Argument schema} which is an object that defines command line arguments. * * @internal */ type ExtractOptionValue<A extends ArgSchema> = A['type'] extends 'string' ? ResolveOptionValue<A, string> : A['type'] extends 'boolean' ? ResolveOptionValue<A, boolean> : A['type'] extends 'number' ? ResolveOptionValue<A, number> : A['type'] extends 'positional' ? ResolveOptionValue<A, string> : A['type'] extends 'enum' ? A['choices'] extends string[] | readonly string[] ? ResolveOptionValue<A, A['choices'][number]> : never : A['type'] extends 'custom' ? IsFunction<A['parse']> extends true ? ResolveOptionValue<A, ReturnType<NonNullable<A['parse']>>> : never : ResolveOptionValue<A, string | boolean | number>; type ResolveOptionValue<A extends ArgSchema, T> = A['multiple'] extends true ? T[] : T; /** * Resolved argument values. * * @typeParam A - {@link Arguments | Args} which is an object that defines the command line arguments. * @typeParam V - Resolvable argument values. * * @internal */ type ResolveArgValues<A extends Args, V extends Record<keyof A, unknown>> = { -readonly [Arg in keyof A]?: V[Arg] } & FilterArgs<A, V, 'default'> & FilterArgs<A, V, 'required'> & FilterPositionalArgs<A, V> extends infer P ? { [K in keyof P]: P[K] } : never; /** * Filters the arguments based on their default values. * * @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments. * @typeParam V - Resolvable argument values. * @typeParam K - Key of the {@link ArgSchema | argument schema} to filter by. * * @internal */ type FilterArgs<A extends Args, V extends Record<keyof A, unknown>, K extends keyof ArgSchema> = { [Arg in keyof A as A[Arg][K] extends {} ? Arg : never]: V[Arg] }; /** * Filters positional arguments from the argument schema. * * @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments. * @typeParam V - Resolvable argument values. * * @internal */ type FilterPositionalArgs<A extends Args, V extends Record<keyof A, unknown>> = { [Arg in keyof A as A[Arg]['type'] extends 'positional' ? Arg : never]: V[Arg] }; /** * An arguments for {@link resolveArgs | resolve arguments}. */ interface ResolveArgs { /** * Whether to group short arguments. * * @see guideline 5 in https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html * * @default false */ shortGrouping?: boolean; /** * Skip positional arguments index. * * @default -1 */ skipPositional?: number; /** * Whether to convert the argument name to kebab-case. This option is applied to all arguments as `toKebab: true`, if set to `true`. * * @default false */ toKebab?: boolean; } /** * Tracks which arguments were explicitly provided by the user. * * Each property indicates whether the corresponding argument was explicitly * provided (true) or is using a default value or not provided (false). * * @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments. */ type ArgExplicitlyProvided<A extends Args> = { [K in keyof A]: boolean }; /** * Resolve command line arguments. * * @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments. * * @param args - An arguments that contains {@link ArgSchema | arguments schema}. * @param tokens - An array of {@link ArgToken | tokens}. * @param resolveArgs - An arguments that contains {@link ResolveArgs | resolve arguments}. * @returns An object that contains the values of the arguments, positional arguments, rest arguments, {@link AggregateError | validation errors}, and explicit provision status. * * @example * ```typescript * // passed tokens: --port 3000 * * const { values, explicit } = resolveArgs({ * port: { * type: 'number', * default: 8080 * }, * host: { * type: 'string', * default: 'localhost' * } * }, parsedTokens) * * values.port // 3000 * values.host // 'localhost' * * explicit.port // true (explicitly provided) * explicit.host // false (not provided, fallback to default) * ``` */ declare function resolveArgs<A extends Args>(args: A, tokens: ArgToken[], { shortGrouping, skipPositional, toKebab }?: ResolveArgs): { values: ArgValues<A>; positionals: string[]; rest: string[]; error: AggregateError | undefined; explicit: ArgExplicitlyProvided<A>; }; /** * An error type for {@link ArgResolveError}. */ type ArgResolveErrorType = 'type' | 'required' | 'conflict'; /** * An error that occurs when resolving arguments. * This error is thrown when the argument is not valid. */ declare class ArgResolveError extends Error { name: string; schema: ArgSchema; type: ArgResolveErrorType; /** * Create an `ArgResolveError` instance. * * @param message - the error message * @param name - the name of the argument * @param type - the type of the error, either 'type' or 'required' * @param schema - the argument schema that caused the error */ constructor(message: string, name: string, type: ArgResolveErrorType, schema: ArgSchema); } //#endregion export { ArgExplicitlyProvided, ArgResolveError, ArgResolveErrorType, ArgSchema, ArgValues, Args, ExtractOptionValue, FilterArgs, FilterPositionalArgs, ResolveArgValues, ResolveArgs, resolveArgs };