113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
import { JUNCTION_FIELDS, PIPE_FIELDS, type ElementType } from "./constants.js";
|
|
import { CliError } from "./errors.js";
|
|
import type { OptionSchema, ParsedOptionValue, ParsedOptions } from "./types.js";
|
|
|
|
export function requireValue(argv: string[], index: number, option: string): string {
|
|
const value = argv[index];
|
|
if (!value || value.startsWith("--")) throw new CliError("CLI 参数错误", "MISSING_OPTION_VALUE", `${option} requires a value`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function parseIntStrict(value: string, option: string): number {
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(parsed)) throw new CliError("CLI 参数错误", "INVALID_INTEGER", `${option} must be an integer`, 2);
|
|
return parsed;
|
|
}
|
|
|
|
export function parseFloatStrict(value: string, option: string): number {
|
|
const parsed = Number.parseFloat(value);
|
|
if (!Number.isFinite(parsed)) throw new CliError("CLI 参数错误", "INVALID_NUMBER", `${option} must be a number`, 2);
|
|
return parsed;
|
|
}
|
|
|
|
export function parseOptions(argv: string[], schema: OptionSchema = {}): ParsedOptions {
|
|
const values: Record<string, ParsedOptionValue> = {};
|
|
const positionals: string[] = [];
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i]!;
|
|
if (!arg.startsWith("--")) {
|
|
positionals.push(arg);
|
|
continue;
|
|
}
|
|
const name = arg.slice(2);
|
|
const kind = schema[name] ?? "string";
|
|
if (kind === "boolean") {
|
|
values[name] = true;
|
|
} else {
|
|
const raw = requireValue(argv, ++i, arg);
|
|
if (kind === "repeat") values[name] = [...asStringArray(values[name]), raw];
|
|
else if (kind === "number") values[name] = parseFloatStrict(raw, arg);
|
|
else if (kind === "integer") values[name] = parseIntStrict(raw, arg);
|
|
else values[name] = raw;
|
|
}
|
|
}
|
|
return { values, positionals };
|
|
}
|
|
|
|
export function required(values: Record<string, ParsedOptionValue>, name: string): string | number | boolean | string[] {
|
|
const value = values[name];
|
|
if (value === undefined || value === null || value === "") {
|
|
throw new CliError("CLI 参数错误", "MISSING_PARAMETER", `Missing option '--${name}'`, 2);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function requiredString(values: Record<string, ParsedOptionValue>, name: string): string {
|
|
const value = required(values, name);
|
|
if (typeof value !== "string") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a string`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function requiredNumber(values: Record<string, ParsedOptionValue>, name: string): number {
|
|
const value = required(values, name);
|
|
if (typeof value !== "number") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a number`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function requiredStringArray(values: Record<string, ParsedOptionValue>, name: string): string[] {
|
|
const value = required(values, name);
|
|
if (!Array.isArray(value)) throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be repeatable`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function optionalString(values: Record<string, ParsedOptionValue>, name: string): string | undefined {
|
|
const value = values[name];
|
|
if (value === undefined) return undefined;
|
|
if (typeof value !== "string") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a string`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function optionalNumber(values: Record<string, ParsedOptionValue>, name: string): number | undefined {
|
|
const value = values[name];
|
|
if (value === undefined) return undefined;
|
|
if (typeof value !== "number") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a number`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function optionalStringArray(values: Record<string, ParsedOptionValue>, name: string): string[] | undefined {
|
|
const value = values[name];
|
|
if (value === undefined) return undefined;
|
|
if (!Array.isArray(value)) throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be repeatable`, 2);
|
|
return value;
|
|
}
|
|
|
|
export function asStringArray(value: ParsedOptionValue | undefined): string[] {
|
|
if (value === undefined) return [];
|
|
if (Array.isArray(value)) return value;
|
|
throw new CliError("CLI 参数错误", "INVALID_PARAMETER", "repeat option received a non-array value", 2);
|
|
}
|
|
|
|
export function validateChoice<T extends readonly string[]>(value: string, valid: T, option: string): T[number] {
|
|
if (!valid.includes(value)) {
|
|
throw new CliError("CLI 参数错误", `INVALID_${option.replace(/^--/, "").replaceAll("-", "_").toUpperCase()}`, `${option} must be one of: ${valid.join(", ")}`, 2);
|
|
}
|
|
return value as T[number];
|
|
}
|
|
|
|
export function fieldsFor(type: ElementType): typeof PIPE_FIELDS | typeof JUNCTION_FIELDS {
|
|
if (type === "pipe") return PIPE_FIELDS;
|
|
if (type === "junction") return JUNCTION_FIELDS;
|
|
throw new CliError("CLI 参数错误", "INVALID_TYPE", "--type must be one of: pipe, junction", 2);
|
|
}
|
|
|