Skip to content
Source
Sign inSign up

cliArgs.ts

112 lines2.6 KB

1export interface ParsedCliArgs {
2 readonly flags: Record<string, string | null>;
3 readonly positionals: string[];
4}
5​
6export interface ParseCliArgsOptions {
7 readonly booleanFlags?: readonly string[];
8}
9​
10export function tokenizeCliArgs(args?: string): ReadonlyArray<string> {
11 const input = args;
12 if (!input) return [];
13​
14 const tokens: string[] = [];
15 let current = "";
16 let quote: "'" | '"' | undefined;
17 let quoted = false;
18​
19 for (let index = 0; index < input.length; index++) {
20 const char = input[index];
21 if (char === undefined) continue;
22​
23 if (quote) {
24 if (char === quote) {
25 quote = undefined;
26 quoted = true;
27 } else if (char === "\\" && quote === '"') {
28 const next = input[index + 1];
29 if (next !== undefined && ['"', "\\", "$", "`"].includes(next)) {
30 current += next;
31 index++;
32 } else {
33 current += char;
34 }
35 } else {
36 current += char;
37 }
38 continue;
39 }
40​
41 if (char === "'" || char === '"') {
42 quote = char;
43 quoted = true;
44 } else if (/\s/.test(char)) {
45 if (current || quoted) {
46 tokens.push(current);
47 current = "";
48 quoted = false;
49 }
50 } else if (char === "\\") {
51 const next = input[index + 1];
52 if (next !== undefined && /[\s'"]/.test(next)) {
53 current += next;
54 index++;
55 } else {
56 current += char;
57 }
58 } else {
59 current += char;
60 }
61 }
62​
63 if (current || quoted) tokens.push(current);
64 return tokens;
66​
67export function parseCliArgs(
68 args: string | readonly string[],
69 options?: ParseCliArgsOptions,
70): ParsedCliArgs {
71 const tokens = typeof args === "string" ? tokenizeCliArgs(args) : Array.from(args);
72 const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined;
73​
74 const flags: Record<string, string | null> = {};
75 const positionals: string[] = [];
76​
77 for (let i = 0; i < tokens.length; i++) {
78 const token = tokens[i]!;
79​
80 if (token === "--") {
81 positionals.push(...tokens.slice(i + 1));
82 break;
83 }
84​
85 if (token.startsWith("--")) {
86 const rest = token.slice(2);
87​
88 const eqIndex = rest.indexOf("=");
89 if (eqIndex !== -1) {
90 flags[rest.slice(0, eqIndex)] = rest.slice(eqIndex + 1);
91 continue;
92 }
93​
94 if (booleanSet?.has(rest)) {
95 flags[rest] = null;
96 continue;
97 }
98​
99 const next = tokens[i + 1];
100 if (next !== undefined && !next.startsWith("--")) {
101 flags[rest] = next;
102 i++;
103 } else {
104 flags[rest] = null;
105 }
106 } else {
107 positionals.push(token);
108 }
109 }
110​
111 return { flags, positionals };