-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathargs.ts
More file actions
127 lines (117 loc) · 4.19 KB
/
args.ts
File metadata and controls
127 lines (117 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { kebabCase, camelCase } from "scule";
import { parseRawArgs } from "./_parser";
import type { Arg, ArgsDef, ParsedArgs, ArgType } from "./types";
import { CLIError, toArray } from "./_utils";
export function parseArgs<T extends ArgsDef = ArgsDef>(
rawArgs: string[],
argsDef: ArgsDef,
): ParsedArgs<T> {
const parseOptions = {
boolean: [] as string[],
string: [] as string[],
number: [] as string[],
enum: [] as (number | string)[],
mixed: [] as string[],
alias: {} as Record<string, string | string[]>,
default: {} as Record<string, boolean | number | string>,
};
const args = resolveArgs(argsDef);
for (const arg of args) {
if (arg.type === "positional") {
continue;
}
// eslint-disable-next-line unicorn/prefer-switch
if (arg.type === "string" || arg.type === "number") {
parseOptions.string.push(arg.name);
} else if (arg.type === "boolean") {
parseOptions.boolean.push(arg.name);
} else if (arg.type === "enum") {
parseOptions.enum.push(...(arg.options || []));
}
if (arg.default !== undefined) {
parseOptions.default[arg.name] = arg.default;
}
if (arg.alias) {
parseOptions.alias[arg.name] = arg.alias;
}
}
const parsed = parseRawArgs(rawArgs, parseOptions);
const [...positionalArguments] = parsed._;
const parsedArgsProxy = new Proxy(parsed, {
get(target: ParsedArgs<any>, prop: string) {
return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
},
});
for (const [, arg] of args.entries()) {
// eslint-disable-next-line unicorn/prefer-switch
if (arg.type === "positional") {
const nextPositionalArgument = positionalArguments.shift();
if (nextPositionalArgument !== undefined) {
parsedArgsProxy[arg.name] = nextPositionalArgument;
} else if (arg.default === undefined && arg.required !== false) {
throw new CLIError(
`Missing required positional argument: ${arg.name.toUpperCase()}`,
"EARG",
);
} else {
parsedArgsProxy[arg.name] = arg.default;
}
} else if (arg.type === "enum") {
const argument = parsedArgsProxy[arg.name];
const options = arg.options || [];
if (
argument !== undefined &&
options.length > 0 &&
!options.includes(argument)
) {
throw new CLIError(
`Invalid value for argument: \`--${arg.name}\` (\`${argument}\`). Expected one of: ${options.map((o) => `\`${o}\``).join(", ")}.`,
"EARG",
);
}
} else if (arg.type === "number") {
const _originalValue = parsedArgsProxy[arg.name];
parsedArgsProxy[arg.name] = Number.parseFloat(
parsedArgsProxy[arg.name] as string,
);
if (Number.isNaN(parsedArgsProxy[arg.name])) {
throw new CLIError(
`Invalid value for argument: \`--${arg.name}\` (\`${_originalValue}\`). Expected a number.`,
"EARG",
);
}
} else if (arg.required && parsedArgsProxy[arg.name] === undefined) {
throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
}
}
return parsedArgsProxy as ParsedArgs<T>;
}
export function resolveArgs(argsDef: ArgsDef): Arg[] {
const args: Arg[] = [];
for (const [name, argDef] of Object.entries(argsDef || {})) {
args.push({
...argDef,
name,
alias: toArray((argDef as any).alias),
});
}
return args;
}
export async function resolveArgsValidate<T extends ArgsDef = ArgsDef>(
parsedArgs: ParsedArgs<T>,
argsDef: ArgsDef,
): Promise<string | undefined> {
for (const [name, argDef] of Object.entries(argsDef || {})) {
const value = parsedArgs[name] as never;
if (!argDef.validate?.verify) {
continue;
}
const result = await argDef.validate.verify(value);
// For optional arguments, validation may always fail depending on the implementation of the validation function.
// If notToThrowCLIError is set, then the verification result can be prevented from propagating to the caller
if (typeof result === "string" && !argDef.validate.notToThrowCLIError) {
const message = result ? ` - ${result}` : "";
return `Argument validation failed: ${name}${message}`;
}
}
}