cli-kiss 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +696 -734
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +2 -3
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/.vitepress/theme/style.css +4 -0
- package/docs/guide/01_getting_started.md +12 -13
- package/docs/guide/02_commands.md +71 -52
- package/docs/guide/03_options.md +25 -33
- package/docs/guide/04_positionals.md +45 -55
- package/docs/guide/05_types.md +66 -66
- package/docs/guide/06_run.md +28 -40
- package/docs/index.md +8 -3
- package/docs/public/favicon.ico +0 -0
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/lib/Command.ts +45 -123
- package/src/lib/Operation.ts +23 -32
- package/src/lib/Option.ts +150 -170
- package/src/lib/Positional.ts +44 -94
- package/src/lib/Reader.ts +123 -99
- package/src/lib/Run.ts +86 -45
- package/src/lib/Type.ts +246 -156
- package/src/lib/Typo.ts +98 -107
- package/src/lib/Usage.ts +163 -82
- package/tests/unit.Reader.aliases.ts +31 -15
- package/tests/unit.Reader.commons.ts +99 -43
- package/tests/unit.Reader.parsings.ts +50 -0
- package/tests/unit.Reader.shortBig.ts +75 -31
- package/tests/unit.command.execute.ts +86 -43
- package/tests/unit.command.usage.ts +88 -82
- package/tests/unit.runner.colors.ts +197 -0
- package/tests/unit.runner.cycle.ts +77 -63
- package/tests/unit.runner.errors.ts +23 -15
package/src/lib/Positional.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
import { ReaderPositionals } from "./Reader";
|
|
2
2
|
import { Type } from "./Type";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
TypoString,
|
|
6
|
-
typoStyleLogic,
|
|
7
|
-
typoStyleUserInput,
|
|
8
|
-
TypoText,
|
|
9
|
-
} from "./Typo";
|
|
3
|
+
import { TypoError, TypoString, typoStyleUserInput, TypoText } from "./Typo";
|
|
4
|
+
import { UsagePositional } from "./Usage";
|
|
10
5
|
|
|
11
6
|
/**
|
|
12
|
-
* A
|
|
13
|
-
*
|
|
14
|
-
* Created with {@link positionalRequired}, {@link positionalOptional}, or
|
|
15
|
-
* {@link positionalVariadics} and passed via the `positionals` array of
|
|
16
|
-
* {@link operation}, consumed in declaration order.
|
|
7
|
+
* A positional argument. Created with {@link positionalRequired}, {@link positionalOptional},
|
|
8
|
+
* or {@link positionalVariadics}.
|
|
17
9
|
*
|
|
18
10
|
* @typeParam Value - Decoded value type.
|
|
19
11
|
*/
|
|
20
12
|
export type Positional<Value> = {
|
|
21
13
|
/**
|
|
22
|
-
* Returns metadata
|
|
14
|
+
* Returns metadata for the `Positionals:` section.
|
|
23
15
|
*/
|
|
24
|
-
generateUsage():
|
|
16
|
+
generateUsage(): UsagePositional;
|
|
25
17
|
/**
|
|
26
18
|
* Consumes the next positional token from `readerPositionals`.
|
|
27
19
|
* Returns a decoder that produces the final value.
|
|
@@ -45,63 +37,36 @@ export type PositionalDecoder<Value> = {
|
|
|
45
37
|
decodeValue(): Value;
|
|
46
38
|
};
|
|
47
39
|
|
|
48
|
-
/**
|
|
49
|
-
* Human-readable metadata for a single positional argument, used to render the
|
|
50
|
-
* `Positionals:` section of the help output produced by {@link usageToStyledLines}.
|
|
51
|
-
*/
|
|
52
|
-
export type PositionalUsage = {
|
|
53
|
-
/**
|
|
54
|
-
* Help text.
|
|
55
|
-
*/
|
|
56
|
-
description: string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
* Short note shown in parentheses.
|
|
59
|
-
*/
|
|
60
|
-
hint: string | undefined;
|
|
61
|
-
/**
|
|
62
|
-
* Placeholder label shown in the usage line and the `Positionals:` section.
|
|
63
|
-
* Required: `<NAME>`, optional: `[NAME]`, variadic: `[NAME]...`.
|
|
64
|
-
*/
|
|
65
|
-
label: Uppercase<string>;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
40
|
/**
|
|
69
41
|
* Creates a required positional — missing token throws {@link TypoError}.
|
|
70
|
-
* Label defaults to uppercased `type.content` in angle brackets (e.g. `<STRING>`).
|
|
71
42
|
*
|
|
72
43
|
* @typeParam Value - Type produced by the decoder.
|
|
73
44
|
*
|
|
74
|
-
* @param definition - Positional configuration.
|
|
75
45
|
* @param definition.description - Help text.
|
|
76
46
|
* @param definition.hint - Short note shown in parentheses.
|
|
77
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
78
47
|
* @param definition.type - Decoder for the raw token.
|
|
79
48
|
* @returns A {@link Positional}`<Value>`.
|
|
80
49
|
*
|
|
81
50
|
* @example
|
|
82
51
|
* ```ts
|
|
83
52
|
* const namePositional = positionalRequired({
|
|
84
|
-
* type:
|
|
85
|
-
* label: "NAME",
|
|
53
|
+
* type: type("name"),
|
|
86
54
|
* description: "The name to greet",
|
|
87
55
|
* });
|
|
88
|
-
* //
|
|
56
|
+
* // Usage:
|
|
57
|
+
* // my-cli Alice → "Alice"
|
|
89
58
|
* ```
|
|
90
59
|
*/
|
|
91
60
|
export function positionalRequired<Value>(definition: {
|
|
92
61
|
description?: string;
|
|
93
62
|
hint?: string;
|
|
94
|
-
label?: Uppercase<string>;
|
|
95
63
|
type: Type<Value>;
|
|
96
64
|
}): Positional<Value> {
|
|
97
|
-
const
|
|
65
|
+
const { description, hint, type } = definition;
|
|
66
|
+
const label = `<${type.content}>`;
|
|
98
67
|
return {
|
|
99
68
|
generateUsage() {
|
|
100
|
-
return {
|
|
101
|
-
description: definition.description,
|
|
102
|
-
hint: definition.hint,
|
|
103
|
-
label: label as Uppercase<string>,
|
|
104
|
-
};
|
|
69
|
+
return { description, hint, label };
|
|
105
70
|
},
|
|
106
71
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
107
72
|
const positional = readerPositionals.consumePositional();
|
|
@@ -124,14 +89,11 @@ export function positionalRequired<Value>(definition: {
|
|
|
124
89
|
|
|
125
90
|
/**
|
|
126
91
|
* Creates an optional positional — absent token falls back to `default()`.
|
|
127
|
-
* Label defaults to uppercased `type.content` in square brackets (e.g. `[STRING]`).
|
|
128
92
|
*
|
|
129
93
|
* @typeParam Value - Type produced by the decoder (or the default).
|
|
130
94
|
*
|
|
131
|
-
* @param definition - Positional configuration.
|
|
132
95
|
* @param definition.description - Help text.
|
|
133
96
|
* @param definition.hint - Short note shown in parentheses.
|
|
134
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
135
97
|
* @param definition.type - Decoder for the raw token.
|
|
136
98
|
* @param definition.default - Value when absent. Throw to make it required.
|
|
137
99
|
* @returns A {@link Positional}`<Value>`.
|
|
@@ -139,30 +101,26 @@ export function positionalRequired<Value>(definition: {
|
|
|
139
101
|
* @example
|
|
140
102
|
* ```ts
|
|
141
103
|
* const greeteePositional = positionalOptional({
|
|
142
|
-
* type:
|
|
143
|
-
* label: "NAME",
|
|
104
|
+
* type: type("name"),
|
|
144
105
|
* description: "Name to greet (default: world)",
|
|
145
106
|
* default: () => "world",
|
|
146
107
|
* });
|
|
147
|
-
* //
|
|
148
|
-
* //
|
|
108
|
+
* // Usage:
|
|
109
|
+
* // my-cli → "world"
|
|
110
|
+
* // my-cli Alice → "Alice"
|
|
149
111
|
* ```
|
|
150
112
|
*/
|
|
151
113
|
export function positionalOptional<Value>(definition: {
|
|
152
114
|
description?: string;
|
|
153
115
|
hint?: string;
|
|
154
|
-
label?: Uppercase<string>;
|
|
155
116
|
type: Type<Value>;
|
|
156
117
|
default: () => Value;
|
|
157
118
|
}): Positional<Value> {
|
|
158
|
-
const
|
|
119
|
+
const { description, hint, type } = definition;
|
|
120
|
+
const label = `[${type.content}]`;
|
|
159
121
|
return {
|
|
160
122
|
generateUsage() {
|
|
161
|
-
return {
|
|
162
|
-
description: definition.description,
|
|
163
|
-
hint: definition.hint,
|
|
164
|
-
label: label as Uppercase<string>,
|
|
165
|
-
};
|
|
123
|
+
return { description, hint, label };
|
|
166
124
|
},
|
|
167
125
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
168
126
|
const positional = readerPositionals.consumePositional();
|
|
@@ -172,13 +130,7 @@ export function positionalOptional<Value>(definition: {
|
|
|
172
130
|
try {
|
|
173
131
|
return definition.default();
|
|
174
132
|
} catch (error) {
|
|
175
|
-
|
|
176
|
-
new TypoText(
|
|
177
|
-
new TypoString(label, typoStyleUserInput),
|
|
178
|
-
new TypoString(`: Failed to get default value`),
|
|
179
|
-
),
|
|
180
|
-
error,
|
|
181
|
-
);
|
|
133
|
+
throwsWhenFailedToGetDefault(label);
|
|
182
134
|
}
|
|
183
135
|
}
|
|
184
136
|
return decodeValue(label, definition.type, positional);
|
|
@@ -190,47 +142,41 @@ export function positionalOptional<Value>(definition: {
|
|
|
190
142
|
|
|
191
143
|
/**
|
|
192
144
|
* Creates a variadic positional that collects zero or more remaining tokens into an array.
|
|
193
|
-
*
|
|
145
|
+
* Optionally stops at `endDelimiter` (consumed, not included).
|
|
194
146
|
*
|
|
195
147
|
* @typeParam Value - Type produced by the decoder for each token.
|
|
196
148
|
*
|
|
197
|
-
* @param definition - Positional configuration.
|
|
198
149
|
* @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
|
|
199
150
|
* @param definition.description - Help text.
|
|
200
151
|
* @param definition.hint - Short note shown in parentheses.
|
|
201
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
202
152
|
* @param definition.type - Decoder applied to each token.
|
|
203
153
|
* @returns A {@link Positional}`<Array<Value>>`.
|
|
204
154
|
*
|
|
205
155
|
* @example
|
|
206
156
|
* ```ts
|
|
207
157
|
* const filesPositional = positionalVariadics({
|
|
208
|
-
* type:
|
|
209
|
-
* label: "FILE",
|
|
158
|
+
* type: typePath(),
|
|
210
159
|
* description: "Files to process",
|
|
211
160
|
* });
|
|
212
|
-
* //
|
|
213
|
-
* //
|
|
161
|
+
* // Usage:
|
|
162
|
+
* // my-cli → []
|
|
163
|
+
* // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
|
|
214
164
|
* ```
|
|
215
165
|
*/
|
|
216
166
|
export function positionalVariadics<Value>(definition: {
|
|
217
167
|
endDelimiter?: string;
|
|
218
168
|
description?: string;
|
|
219
169
|
hint?: string;
|
|
220
|
-
label?: Uppercase<string>;
|
|
221
170
|
type: Type<Value>;
|
|
222
171
|
}): Positional<Array<Value>> {
|
|
223
|
-
const
|
|
172
|
+
const { description, hint, type } = definition;
|
|
173
|
+
const labelSingle = `[${type.content}]`;
|
|
174
|
+
const labelMultiple =
|
|
175
|
+
`${labelSingle}...` +
|
|
176
|
+
(definition.endDelimiter ? ` ["${definition.endDelimiter}"]` : "");
|
|
224
177
|
return {
|
|
225
178
|
generateUsage() {
|
|
226
|
-
return {
|
|
227
|
-
description: definition.description,
|
|
228
|
-
hint: definition.hint,
|
|
229
|
-
label: (`${label}...` +
|
|
230
|
-
(definition.endDelimiter
|
|
231
|
-
? `["${definition.endDelimiter}"]`
|
|
232
|
-
: "")) as Uppercase<string>,
|
|
233
|
-
};
|
|
179
|
+
return { description, hint, label: labelMultiple };
|
|
234
180
|
},
|
|
235
181
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
236
182
|
const positionals = new Array<string>();
|
|
@@ -246,9 +192,9 @@ export function positionalVariadics<Value>(definition: {
|
|
|
246
192
|
}
|
|
247
193
|
return {
|
|
248
194
|
decodeValue() {
|
|
249
|
-
return positionals.map((positional) =>
|
|
250
|
-
|
|
251
|
-
|
|
195
|
+
return positionals.map((positional) =>
|
|
196
|
+
decodeValue(labelSingle, definition.type, positional),
|
|
197
|
+
);
|
|
252
198
|
},
|
|
253
199
|
};
|
|
254
200
|
},
|
|
@@ -262,11 +208,15 @@ function decodeValue<Value>(
|
|
|
262
208
|
): Value {
|
|
263
209
|
return TypoError.tryWithContext(
|
|
264
210
|
() => type.decoder(input),
|
|
265
|
-
() =>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
211
|
+
() => new TypoText(new TypoString(label, typoStyleUserInput)),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function throwsWhenFailedToGetDefault(label: string): never {
|
|
216
|
+
throw new TypoError(
|
|
217
|
+
new TypoText(
|
|
218
|
+
new TypoString(label, typoStyleUserInput),
|
|
219
|
+
new TypoString(`: Failed to get default value`),
|
|
220
|
+
),
|
|
271
221
|
);
|
|
272
222
|
}
|
package/src/lib/Reader.ts
CHANGED
|
@@ -7,44 +7,58 @@ import {
|
|
|
7
7
|
} from "./Typo";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Opaque key
|
|
11
|
-
* Returned by {@link ReaderArgs.registerOption}; passed to {@link ReaderArgs.getOptionValues}.
|
|
10
|
+
* Opaque key returned by {@link ReaderArgs.registerOption}.
|
|
12
11
|
*/
|
|
13
12
|
export type ReaderOptionKey = (string | { __brand: "ReaderOptionKey" }) & {
|
|
14
13
|
__brand: "ReaderOptionKey";
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
17
|
+
* Parsing behaviour for a registered option, passed to {@link ReaderArgs.registerOption}.
|
|
18
|
+
*/
|
|
19
|
+
export type ReaderOptionParsing = {
|
|
20
|
+
consumeShortGroup: boolean; // TODO - this doesnt matter when no short option
|
|
21
|
+
consumeNextArg: (
|
|
22
|
+
inlined: string | null,
|
|
23
|
+
separated: Array<string>,
|
|
24
|
+
nextArg: string | undefined,
|
|
25
|
+
) => boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Result of parsing an option, including its inlined value and any following separated values.
|
|
30
|
+
*/
|
|
31
|
+
export type ReaderOptionValue = {
|
|
32
|
+
inlined: string | null;
|
|
33
|
+
separated: Array<string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Option registration/query interface. Subset of {@link ReaderArgs}.
|
|
20
38
|
*/
|
|
21
39
|
export type ReaderOptions = {
|
|
22
40
|
/**
|
|
23
|
-
* Registers an option
|
|
41
|
+
* Registers an option; all `longs` and `shorts` share the same key.
|
|
24
42
|
*
|
|
25
43
|
* @param definition.longs - Long-form names (without `--`).
|
|
26
44
|
* @param definition.shorts - Short-form names (without `-`).
|
|
27
|
-
* @param definition.
|
|
45
|
+
* @param definition.parsing - Parsing behaviour.
|
|
28
46
|
* @returns A {@link ReaderOptionKey} for later retrieval.
|
|
29
47
|
* @throws `Error` if a name is already registered or short names overlap.
|
|
30
48
|
*/
|
|
31
49
|
registerOption(definition: {
|
|
32
50
|
longs: Array<string>;
|
|
33
51
|
shorts: Array<string>;
|
|
34
|
-
|
|
52
|
+
parsing: ReaderOptionParsing;
|
|
35
53
|
}): ReaderOptionKey;
|
|
36
54
|
/**
|
|
37
|
-
* Returns all values collected for
|
|
38
|
-
*
|
|
39
|
-
* @param key - Key from {@link ReaderOptions.registerOption}.
|
|
40
|
-
* @returns Raw string values, one per occurrence; empty if never provided.
|
|
41
|
-
* @throws `Error` if `key` was not registered.
|
|
55
|
+
* Returns all values collected for `key`.
|
|
42
56
|
*/
|
|
43
|
-
getOptionValues(key: ReaderOptionKey): Array<
|
|
57
|
+
getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue>;
|
|
44
58
|
};
|
|
45
59
|
|
|
46
60
|
/**
|
|
47
|
-
* Positional
|
|
61
|
+
* Positional consumption interface. Subset of {@link ReaderArgs}.
|
|
48
62
|
*/
|
|
49
63
|
export type ReaderPositionals = {
|
|
50
64
|
/**
|
|
@@ -69,10 +83,9 @@ export class ReaderArgs {
|
|
|
69
83
|
#args: ReadonlyArray<string>;
|
|
70
84
|
#parsedIndex: number;
|
|
71
85
|
#parsedDouble: boolean;
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#resultByKey: Map<ReaderOptionKey, Array<string>>;
|
|
86
|
+
#optionContextByLong: Map<string, ReaderOptionContext>;
|
|
87
|
+
#optionContextByShort: Map<string, ReaderOptionContext>;
|
|
88
|
+
#optionContextByKey: Map<ReaderOptionKey, ReaderOptionContext>;
|
|
76
89
|
|
|
77
90
|
/**
|
|
78
91
|
* @param args - Raw CLI tokens (e.g. `process.argv.slice(2)`). Not mutated.
|
|
@@ -81,27 +94,25 @@ export class ReaderArgs {
|
|
|
81
94
|
this.#args = args;
|
|
82
95
|
this.#parsedIndex = 0;
|
|
83
96
|
this.#parsedDouble = false;
|
|
84
|
-
this.#
|
|
85
|
-
this.#
|
|
86
|
-
this.#
|
|
87
|
-
this.#resultByKey = new Map();
|
|
97
|
+
this.#optionContextByLong = new Map();
|
|
98
|
+
this.#optionContextByShort = new Map();
|
|
99
|
+
this.#optionContextByKey = new Map();
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/**
|
|
91
103
|
* Registers an option; all `longs` and `shorts` share the same key.
|
|
92
|
-
* Short names
|
|
93
|
-
* but must not be prefixes of one another.
|
|
104
|
+
* Short names must not be prefixes of one another.
|
|
94
105
|
*
|
|
95
106
|
* @param definition.longs - Long-form names (without `--`).
|
|
96
107
|
* @param definition.shorts - Short-form names (without `-`).
|
|
97
|
-
* @param definition.
|
|
108
|
+
* @param definition.parsing - Parsing behaviour.
|
|
98
109
|
* @returns A {@link ReaderOptionKey} for {@link ReaderArgs.getOptionValues}.
|
|
99
110
|
* @throws `Error` if any name is already registered or short names overlap.
|
|
100
111
|
*/
|
|
101
112
|
registerOption(definition: {
|
|
102
113
|
longs: Array<string>;
|
|
103
114
|
shorts: Array<string>;
|
|
104
|
-
|
|
115
|
+
parsing: ReaderOptionParsing;
|
|
105
116
|
}) {
|
|
106
117
|
const key = [
|
|
107
118
|
...definition.longs.map((long) => `--${long}`),
|
|
@@ -111,7 +122,7 @@ export class ReaderArgs {
|
|
|
111
122
|
if (!this.#isValidOptionName(long)) {
|
|
112
123
|
throw new Error(`Invalid option name: --${long}`);
|
|
113
124
|
}
|
|
114
|
-
if (this.#
|
|
125
|
+
if (this.#optionContextByLong.has(long)) {
|
|
115
126
|
throw new Error(`Option already registered: --${long}`);
|
|
116
127
|
}
|
|
117
128
|
}
|
|
@@ -119,18 +130,18 @@ export class ReaderArgs {
|
|
|
119
130
|
if (!this.#isValidOptionName(short)) {
|
|
120
131
|
throw new Error(`Invalid option name: -${short}`);
|
|
121
132
|
}
|
|
122
|
-
if (this.#
|
|
133
|
+
if (this.#optionContextByShort.has(short)) {
|
|
123
134
|
throw new Error(`Option already registered: -${short}`);
|
|
124
135
|
}
|
|
125
136
|
for (let i = 0; i < short.length; i++) {
|
|
126
137
|
const shortSlice = short.slice(0, i);
|
|
127
|
-
if (this.#
|
|
138
|
+
if (this.#optionContextByShort.has(shortSlice)) {
|
|
128
139
|
throw new Error(
|
|
129
140
|
`Option -${short} overlap with shorter option: -${shortSlice}`,
|
|
130
141
|
);
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
|
-
for (const shortOther of this.#
|
|
144
|
+
for (const shortOther of this.#optionContextByShort.keys()) {
|
|
134
145
|
if (shortOther.startsWith(short)) {
|
|
135
146
|
throw new Error(
|
|
136
147
|
`Option -${short} overlap with longer option: -${shortOther}`,
|
|
@@ -138,35 +149,37 @@ export class ReaderArgs {
|
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
}
|
|
152
|
+
const optionContext = {
|
|
153
|
+
parsing: definition.parsing,
|
|
154
|
+
results: new Array<ReaderOptionValue>(),
|
|
155
|
+
};
|
|
141
156
|
for (const long of definition.longs) {
|
|
142
|
-
this.#
|
|
157
|
+
this.#optionContextByLong.set(long, optionContext);
|
|
143
158
|
}
|
|
144
159
|
for (const short of definition.shorts) {
|
|
145
|
-
this.#
|
|
160
|
+
this.#optionContextByShort.set(short, optionContext);
|
|
146
161
|
}
|
|
147
|
-
this.#
|
|
148
|
-
this.#resultByKey.set(key, new Array<string>());
|
|
162
|
+
this.#optionContextByKey.set(key, optionContext);
|
|
149
163
|
return key;
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
/**
|
|
153
|
-
* Returns all values collected for
|
|
167
|
+
* Returns all values collected for `key`.
|
|
154
168
|
*
|
|
155
169
|
* @param key - Key from {@link ReaderArgs.registerOption}.
|
|
156
|
-
* @returns
|
|
170
|
+
* @returns One entry per occurrence.
|
|
157
171
|
* @throws `Error` if `key` was not registered.
|
|
158
172
|
*/
|
|
159
|
-
getOptionValues(key: ReaderOptionKey): Array<
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
173
|
+
getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue> {
|
|
174
|
+
const optionContext = this.#optionContextByKey.get(key);
|
|
175
|
+
if (optionContext === undefined) {
|
|
162
176
|
throw new Error(`Unregistered option: ${key}`);
|
|
163
177
|
}
|
|
164
|
-
return
|
|
178
|
+
return optionContext.results;
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
/**
|
|
168
|
-
* Returns the next
|
|
169
|
-
* Parse intervening options as side-effects.
|
|
182
|
+
* Returns the next positional token; parses intervening options as a side-effect.
|
|
170
183
|
* All tokens after `--` are treated as positionals.
|
|
171
184
|
*
|
|
172
185
|
* @returns The next positional, or `undefined` when exhausted.
|
|
@@ -175,19 +188,19 @@ export class ReaderArgs {
|
|
|
175
188
|
consumePositional(): string | undefined {
|
|
176
189
|
while (true) {
|
|
177
190
|
const arg = this.#consumeArg();
|
|
178
|
-
if (arg ===
|
|
191
|
+
if (arg === undefined) {
|
|
179
192
|
return undefined;
|
|
180
193
|
}
|
|
181
|
-
if (this.#
|
|
194
|
+
if (!this.#tryConsumeAsOption(arg)) {
|
|
182
195
|
return arg;
|
|
183
196
|
}
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
#consumeArg(): string |
|
|
200
|
+
#consumeArg(): string | undefined {
|
|
188
201
|
const arg = this.#args[this.#parsedIndex];
|
|
189
202
|
if (arg === undefined) {
|
|
190
|
-
return
|
|
203
|
+
return undefined;
|
|
191
204
|
}
|
|
192
205
|
this.#parsedIndex++;
|
|
193
206
|
if (!this.#parsedDouble) {
|
|
@@ -199,9 +212,9 @@ export class ReaderArgs {
|
|
|
199
212
|
return arg;
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
#
|
|
215
|
+
#tryConsumeAsOption(arg: string): boolean {
|
|
203
216
|
if (this.#parsedDouble) {
|
|
204
|
-
return
|
|
217
|
+
return false;
|
|
205
218
|
}
|
|
206
219
|
if (arg.startsWith("--")) {
|
|
207
220
|
const valueIndexStart = arg.indexOf("=");
|
|
@@ -213,80 +226,90 @@ export class ReaderArgs {
|
|
|
213
226
|
arg.slice(valueIndexStart + 1),
|
|
214
227
|
);
|
|
215
228
|
}
|
|
216
|
-
return
|
|
229
|
+
return true;
|
|
217
230
|
}
|
|
218
231
|
if (arg.startsWith("-")) {
|
|
219
232
|
let shortIndexStart = 1;
|
|
220
233
|
let shortIndexEnd = 2;
|
|
221
234
|
while (shortIndexEnd <= arg.length) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (result === false) {
|
|
235
|
+
const short = arg.slice(shortIndexStart, shortIndexEnd);
|
|
236
|
+
const optionContext = this.#optionContextByShort.get(short);
|
|
237
|
+
if (optionContext !== undefined) {
|
|
238
|
+
const rest = arg.slice(shortIndexEnd);
|
|
239
|
+
if (this.#tryConsumeOptionShort(optionContext, short, rest)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
230
242
|
shortIndexStart = shortIndexEnd;
|
|
231
243
|
}
|
|
232
244
|
shortIndexEnd++;
|
|
233
245
|
}
|
|
234
246
|
throw new TypoError(
|
|
235
247
|
new TypoText(
|
|
236
|
-
new TypoString(
|
|
237
|
-
new TypoString(
|
|
248
|
+
new TypoString(`Unexpected unknown option(s): `),
|
|
249
|
+
new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleQuote),
|
|
238
250
|
),
|
|
239
251
|
);
|
|
240
252
|
}
|
|
241
|
-
return
|
|
253
|
+
return false;
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
#consumeOptionLong(long: string,
|
|
256
|
+
#consumeOptionLong(long: string, inlined: string | null): void {
|
|
245
257
|
const constant = `--${long}`;
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
return this.#acknowledgeOption(key, direct);
|
|
250
|
-
}
|
|
251
|
-
const valued = this.#valuedByKey.get(key);
|
|
252
|
-
if (valued) {
|
|
253
|
-
return this.#acknowledgeOption(key, this.#consumeOptionValue(constant));
|
|
254
|
-
}
|
|
255
|
-
return this.#acknowledgeOption(key, "true");
|
|
258
|
+
const optionContext = this.#optionContextByLong.get(long);
|
|
259
|
+
if (optionContext !== undefined) {
|
|
260
|
+
return this.#consumeOptionValues(optionContext, constant, inlined);
|
|
256
261
|
}
|
|
257
262
|
throw new TypoError(
|
|
258
263
|
new TypoText(
|
|
259
|
-
new TypoString(
|
|
260
|
-
new TypoString(
|
|
264
|
+
new TypoString(`Unexpected unknown option: `),
|
|
265
|
+
new TypoString(constant, typoStyleQuote),
|
|
261
266
|
),
|
|
262
267
|
);
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
#tryConsumeOptionShort(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return rest === "";
|
|
270
|
+
#tryConsumeOptionShort(
|
|
271
|
+
optionContext: ReaderOptionContext,
|
|
272
|
+
short: string,
|
|
273
|
+
rest: string,
|
|
274
|
+
): boolean {
|
|
275
|
+
const constant = `-${short}`;
|
|
276
|
+
if (rest.startsWith("=")) {
|
|
277
|
+
this.#consumeOptionValues(optionContext, constant, rest.slice(1));
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
if (rest.length === 0) {
|
|
281
|
+
this.#consumeOptionValues(optionContext, constant, null);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (optionContext.parsing.consumeShortGroup) {
|
|
285
|
+
this.#consumeOptionValues(optionContext, constant, rest);
|
|
286
|
+
return true;
|
|
283
287
|
}
|
|
284
|
-
|
|
288
|
+
this.#consumeOptionValues(optionContext, constant, null);
|
|
289
|
+
return false;
|
|
285
290
|
}
|
|
286
291
|
|
|
287
|
-
#
|
|
292
|
+
#consumeOptionValues(
|
|
293
|
+
optionContext: ReaderOptionContext,
|
|
294
|
+
constant: string,
|
|
295
|
+
inlined: string | null,
|
|
296
|
+
) {
|
|
297
|
+
const separated = new Array<string>();
|
|
298
|
+
while (
|
|
299
|
+
optionContext.parsing.consumeNextArg(
|
|
300
|
+
inlined,
|
|
301
|
+
separated,
|
|
302
|
+
this.#args[this.#parsedIndex],
|
|
303
|
+
)
|
|
304
|
+
) {
|
|
305
|
+
separated.push(this.#consumeOptionValue(constant));
|
|
306
|
+
}
|
|
307
|
+
optionContext.results.push({ inlined, separated });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#consumeOptionValue(constant: string): string {
|
|
288
311
|
const arg = this.#consumeArg();
|
|
289
|
-
if (arg ===
|
|
312
|
+
if (arg === undefined) {
|
|
290
313
|
throw new TypoError(
|
|
291
314
|
new TypoText(
|
|
292
315
|
new TypoString(constant, typoStyleConstants),
|
|
@@ -316,11 +339,12 @@ export class ReaderArgs {
|
|
|
316
339
|
return arg;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
|
-
#acknowledgeOption(key: ReaderOptionKey, value: string) {
|
|
320
|
-
this.getOptionValues(key).push(value);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
342
|
#isValidOptionName(name: string): boolean {
|
|
324
|
-
return name.length > 0 && !name.includes("=");
|
|
343
|
+
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
325
344
|
}
|
|
326
345
|
}
|
|
346
|
+
|
|
347
|
+
type ReaderOptionContext = {
|
|
348
|
+
parsing: ReaderOptionParsing;
|
|
349
|
+
results: Array<ReaderOptionValue>;
|
|
350
|
+
};
|