cli-kiss 0.2.6 → 0.2.8
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 +62 -4
- package/dist/index.d.ts +135 -128
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -1
- package/docs/.vitepress/theme/Layout.vue +16 -0
- package/docs/.vitepress/theme/index.ts +5 -1
- package/docs/.vitepress/theme/style.css +5 -1
- package/docs/guide/02_commands.md +1 -1
- package/docs/guide/03_options.md +11 -11
- package/docs/guide/05_input_types.md +9 -10
- package/docs/guide/06_run_as_cli.md +1 -1
- package/docs/index.md +2 -2
- package/docs/public/favicon.ico +0 -0
- package/docs/public/logo.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/Command.ts +50 -30
- package/src/lib/Operation.ts +29 -21
- package/src/lib/Option.ts +198 -133
- package/src/lib/Positional.ts +46 -24
- package/src/lib/Reader.ts +194 -207
- package/src/lib/Run.ts +19 -8
- package/src/lib/Suggest.ts +78 -0
- package/src/lib/Type.ts +46 -48
- package/src/lib/Typo.ts +72 -47
- package/src/lib/Usage.ts +13 -13
- package/tests/unit.Reader.commons.ts +92 -116
- package/tests/unit.Reader.parsings.ts +14 -26
- package/tests/unit.Reader.shortBig.ts +81 -96
- package/tests/unit.command.aliases.ts +100 -0
- package/tests/unit.command.execute.ts +1 -1
- package/tests/unit.command.usage.ts +12 -6
- package/tests/unit.fuzzed.alternatives.ts +43 -0
- package/tests/unit.runner.colors.ts +11 -35
- package/tests/unit.runner.cycle.ts +181 -128
- package/tests/unit.runner.errors.ts +26 -19
- package/docs/public/hero.png +0 -0
- package/tests/unit.Reader.aliases.ts +0 -62
package/src/lib/Option.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReaderOptionGetter,
|
|
3
|
+
ReaderOptionNextGuard,
|
|
4
|
+
ReaderOptionRestGuard,
|
|
5
|
+
ReaderOptions,
|
|
6
|
+
ReaderOptionValue,
|
|
7
|
+
} from "./Reader";
|
|
2
8
|
import { Type, typeBoolean } from "./Type";
|
|
3
9
|
import {
|
|
4
10
|
TypoError,
|
|
@@ -36,7 +42,7 @@ export type OptionDecoder<Value> = {
|
|
|
36
42
|
/**
|
|
37
43
|
* Returns the decoded option value.
|
|
38
44
|
*
|
|
39
|
-
* @throws
|
|
45
|
+
* @throws if decoding failed.
|
|
40
46
|
*/
|
|
41
47
|
getAndDecodeValue(): Value;
|
|
42
48
|
};
|
|
@@ -44,8 +50,12 @@ export type OptionDecoder<Value> = {
|
|
|
44
50
|
/**
|
|
45
51
|
* Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
|
|
46
52
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
53
|
+
* Syntax: `--long`, `--long=no`, `-s`, `-s=no`.
|
|
54
|
+
* Parsing logic:
|
|
55
|
+
* - absent → default value
|
|
56
|
+
* - `--flag` / `--flag=yes` → `true`
|
|
57
|
+
* - `--flag=no` → `false`
|
|
58
|
+
* - specified more than once → throws.
|
|
49
59
|
*
|
|
50
60
|
* @param definition.long - Long-form name (without `--`).
|
|
51
61
|
* @param definition.short - Short-form name (without `-`).
|
|
@@ -62,11 +72,6 @@ export type OptionDecoder<Value> = {
|
|
|
62
72
|
* short: "v",
|
|
63
73
|
* description: "Enable verbose output",
|
|
64
74
|
* });
|
|
65
|
-
* // Usage:
|
|
66
|
-
* // my-cli → false
|
|
67
|
-
* // my-cli --verbose → true
|
|
68
|
-
* // my-cli --verbose=yes → true
|
|
69
|
-
* // my-cli -v=no → false
|
|
70
75
|
* ```
|
|
71
76
|
*/
|
|
72
77
|
export function optionFlag(definition: {
|
|
@@ -84,28 +89,30 @@ export function optionFlag(definition: {
|
|
|
84
89
|
return { short, long, annotation: "[=no]", description, hint };
|
|
85
90
|
},
|
|
86
91
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
87
|
-
const
|
|
88
|
-
long,
|
|
89
|
-
short,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const resultsGetter = setupOptionAliased(readerOptions, {
|
|
93
|
+
longKey: long,
|
|
94
|
+
shortKey: short,
|
|
95
|
+
aliasLongKeys: aliases?.longs,
|
|
96
|
+
aliasShortKeys: aliases?.shorts,
|
|
97
|
+
restGuard: () => false,
|
|
98
|
+
nextGuard: () => false,
|
|
93
99
|
});
|
|
94
100
|
return {
|
|
95
101
|
getAndDecodeValue() {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
throwSetMultipleTimesError(
|
|
102
|
+
const results = resultsGetter();
|
|
103
|
+
if (results.length > 1) {
|
|
104
|
+
throwSetMultipleTimesError(results.map((r) => r.identifier));
|
|
99
105
|
}
|
|
100
|
-
if (
|
|
106
|
+
if (results.length === 0) {
|
|
101
107
|
return definition.default === undefined
|
|
102
108
|
? false
|
|
103
109
|
: definition.default;
|
|
104
110
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const input = results[0]!.value.inlined;
|
|
112
|
+
if (input === null) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return decodeValue({ long, label: undefined, type, input });
|
|
109
116
|
},
|
|
110
117
|
};
|
|
111
118
|
},
|
|
@@ -115,8 +122,13 @@ export function optionFlag(definition: {
|
|
|
115
122
|
/**
|
|
116
123
|
* Creates an option that accepts exactly one value (e.g. `--output dist/`).
|
|
117
124
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
125
|
+
* Syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
|
|
126
|
+
* Parsing logic:
|
|
127
|
+
* - absent → `fallbackValueIfAbsent()`
|
|
128
|
+
* - `--long value` → decoded with `type.decoder("value")`
|
|
129
|
+
* - more than once → throws
|
|
130
|
+
* - if `impliedValueIfNotInlined` is not provided, then: `--long` / `-s` without a value → throws
|
|
131
|
+
* - if `impliedValueIfNotInlined` is provided, then: `--long` / `-s` without an inline value → `impliedValueIfNotInlined()`
|
|
120
132
|
*
|
|
121
133
|
* @typeParam Value - Type produced by the decoder.
|
|
122
134
|
*
|
|
@@ -126,8 +138,8 @@ export function optionFlag(definition: {
|
|
|
126
138
|
* @param definition.hint - Short note shown in parentheses.
|
|
127
139
|
* @param definition.aliases - Additional names.
|
|
128
140
|
* @param definition.type - Decoder for the raw string value.
|
|
129
|
-
* @param definition.
|
|
130
|
-
* @param definition.
|
|
141
|
+
* @param definition.fallbackValueIfAbsent - Default value when the option is not specified at all.
|
|
142
|
+
* @param definition.impliedValueIfNotInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
|
|
131
143
|
* @returns An {@link Option}`<Value>`.
|
|
132
144
|
*
|
|
133
145
|
* @example
|
|
@@ -137,12 +149,8 @@ export function optionFlag(definition: {
|
|
|
137
149
|
* short: "o",
|
|
138
150
|
* type: typePath(),
|
|
139
151
|
* description: "Output directory",
|
|
140
|
-
*
|
|
152
|
+
* fallbackValueIfAbsent: () => "dist",
|
|
141
153
|
* });
|
|
142
|
-
* // Usage:
|
|
143
|
-
* // my-cli → "dist"
|
|
144
|
-
* // my-cli --output folder → "folder"
|
|
145
|
-
* // my-cli -o folder → "folder"
|
|
146
154
|
* ```
|
|
147
155
|
*/
|
|
148
156
|
export function optionSingleValue<Value>(definition: {
|
|
@@ -152,58 +160,83 @@ export function optionSingleValue<Value>(definition: {
|
|
|
152
160
|
hint?: string;
|
|
153
161
|
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
154
162
|
type: Type<Value>;
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
fallbackValueIfAbsent?: () => Value;
|
|
164
|
+
impliedValueIfNotInlined?: () => Value;
|
|
157
165
|
}): Option<Value> {
|
|
158
166
|
const { long, short, description, hint, aliases, type } = definition;
|
|
159
|
-
const label =
|
|
167
|
+
const label = definition.impliedValueIfNotInlined
|
|
168
|
+
? undefined
|
|
169
|
+
: `<${type.content}>`;
|
|
170
|
+
const annotation = definition.impliedValueIfNotInlined
|
|
171
|
+
? `[=${type.content}]`
|
|
172
|
+
: undefined; // TODO - handle implied value and default better in usage and errors
|
|
160
173
|
return {
|
|
161
174
|
generateUsage() {
|
|
162
|
-
return { short, long, label, description, hint };
|
|
175
|
+
return { short, long, label, annotation, description, hint };
|
|
163
176
|
},
|
|
164
177
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
165
|
-
const
|
|
166
|
-
long,
|
|
167
|
-
short,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
const resultsGetter = setupOptionAliased(readerOptions, {
|
|
179
|
+
longKey: long,
|
|
180
|
+
shortKey: short,
|
|
181
|
+
aliasLongKeys: aliases?.longs,
|
|
182
|
+
aliasShortKeys: aliases?.shorts,
|
|
183
|
+
restGuard: () => {
|
|
184
|
+
if (definition.impliedValueIfNotInlined !== undefined) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
nextGuard: (value) => {
|
|
190
|
+
if (definition.impliedValueIfNotInlined !== undefined) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (value.inlined !== null) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (value.separated.length !== 0) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
178
200
|
},
|
|
179
201
|
});
|
|
180
202
|
return {
|
|
181
203
|
getAndDecodeValue() {
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
throwSetMultipleTimesError(
|
|
204
|
+
const results = resultsGetter();
|
|
205
|
+
if (results.length > 1) {
|
|
206
|
+
throwSetMultipleTimesError(
|
|
207
|
+
results.map((result) => result.identifier),
|
|
208
|
+
);
|
|
185
209
|
}
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
210
|
+
const result = results[0];
|
|
211
|
+
if (result === undefined) {
|
|
212
|
+
if (definition.fallbackValueIfAbsent === undefined) {
|
|
213
|
+
const errorText = makeErrorText({ long, label, type });
|
|
214
|
+
errorText.push(new TypoString(`: Is required, but was not set.`));
|
|
215
|
+
throw new TypoError(errorText);
|
|
216
|
+
}
|
|
188
217
|
try {
|
|
189
|
-
return definition.
|
|
218
|
+
return definition.fallbackValueIfAbsent();
|
|
190
219
|
} catch (error) {
|
|
191
|
-
|
|
220
|
+
const errorText = makeErrorText({ long, label, type });
|
|
221
|
+
errorText.push(new TypoString(`: Failed to get fallback value.`));
|
|
222
|
+
throw new TypoError(errorText, error);
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return decodeValue({ long,
|
|
225
|
+
const inlined = result.value.inlined;
|
|
226
|
+
if (inlined !== null) {
|
|
227
|
+
return decodeValue({ long, label, type, input: inlined });
|
|
197
228
|
}
|
|
198
|
-
if (definition.
|
|
229
|
+
if (definition.impliedValueIfNotInlined !== undefined) {
|
|
199
230
|
try {
|
|
200
|
-
return definition.
|
|
231
|
+
return definition.impliedValueIfNotInlined();
|
|
201
232
|
} catch (error) {
|
|
202
|
-
|
|
233
|
+
const errorText = makeErrorText({ long, label, type });
|
|
234
|
+
errorText.push(new TypoString(`: Failed to get implied value.`));
|
|
235
|
+
throw new TypoError(errorText, error);
|
|
203
236
|
}
|
|
204
237
|
}
|
|
205
|
-
const separated =
|
|
206
|
-
return decodeValue({ long,
|
|
238
|
+
const separated = result.value.separated[0]!;
|
|
239
|
+
return decodeValue({ long, label, type, input: separated });
|
|
207
240
|
},
|
|
208
241
|
};
|
|
209
242
|
},
|
|
@@ -213,8 +246,10 @@ export function optionSingleValue<Value>(definition: {
|
|
|
213
246
|
/**
|
|
214
247
|
* Creates an option that collects every occurrence into an array (e.g. `--file a.ts --file b.ts`).
|
|
215
248
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
249
|
+
* Syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
|
|
250
|
+
* Parsing logic:
|
|
251
|
+
* - absent → `[]`
|
|
252
|
+
* - N occurrences → array of N decoded values in order.
|
|
218
253
|
*
|
|
219
254
|
* @typeParam Value - Type produced by the decoder for each occurrence.
|
|
220
255
|
*
|
|
@@ -235,7 +270,6 @@ export function optionSingleValue<Value>(definition: {
|
|
|
235
270
|
* label: "PATH",
|
|
236
271
|
* description: "Input file (may be repeated)",
|
|
237
272
|
* });
|
|
238
|
-
* // Usage: my-cli --file a.ts --file b.ts → ["a.ts", "b.ts"]
|
|
239
273
|
* ```
|
|
240
274
|
*/
|
|
241
275
|
export function optionRepeatable<Value>(definition: {
|
|
@@ -253,23 +287,28 @@ export function optionRepeatable<Value>(definition: {
|
|
|
253
287
|
return { short, long, label, annotation: " [*]", description, hint };
|
|
254
288
|
},
|
|
255
289
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
256
|
-
const
|
|
257
|
-
long,
|
|
258
|
-
short,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
290
|
+
const resultsGetter = setupOptionAliased(readerOptions, {
|
|
291
|
+
longKey: long,
|
|
292
|
+
shortKey: short,
|
|
293
|
+
aliasLongKeys: aliases?.longs,
|
|
294
|
+
aliasShortKeys: aliases?.shorts,
|
|
295
|
+
restGuard: () => true,
|
|
296
|
+
nextGuard: (value) => {
|
|
297
|
+
if (value.inlined !== null) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (value.separated.length !== 0) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
265
304
|
},
|
|
266
305
|
});
|
|
267
306
|
return {
|
|
268
307
|
getAndDecodeValue() {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const input =
|
|
272
|
-
return decodeValue({ long,
|
|
308
|
+
return resultsGetter().map((result) => {
|
|
309
|
+
const value = result.value;
|
|
310
|
+
const input = value.inlined ?? value.separated[0]!;
|
|
311
|
+
return decodeValue({ long, label, type, input });
|
|
273
312
|
});
|
|
274
313
|
},
|
|
275
314
|
};
|
|
@@ -279,73 +318,99 @@ export function optionRepeatable<Value>(definition: {
|
|
|
279
318
|
|
|
280
319
|
function decodeValue<Value>(params: {
|
|
281
320
|
long: string;
|
|
282
|
-
|
|
283
|
-
label?: string | undefined;
|
|
321
|
+
label: string | undefined;
|
|
284
322
|
type: Type<Value>;
|
|
285
323
|
input: string;
|
|
286
324
|
}): Value {
|
|
325
|
+
const { long, label, type, input } = params;
|
|
287
326
|
return TypoError.tryWithContext(
|
|
288
|
-
() =>
|
|
289
|
-
() => {
|
|
290
|
-
const text = new TypoText();
|
|
291
|
-
if (params.short) {
|
|
292
|
-
text.push(new TypoString(`-${params.short}`, typoStyleConstants));
|
|
293
|
-
text.push(new TypoString(`, `));
|
|
294
|
-
}
|
|
295
|
-
text.push(new TypoString(`--${params.long}`, typoStyleConstants));
|
|
296
|
-
if (params.label) {
|
|
297
|
-
text.push(new TypoString(`: `));
|
|
298
|
-
text.push(new TypoString(params.label, typoStyleUserInput));
|
|
299
|
-
} else {
|
|
300
|
-
text.push(new TypoString(`: `));
|
|
301
|
-
text.push(new TypoString(params.type.content, typoStyleLogic));
|
|
302
|
-
}
|
|
303
|
-
return text;
|
|
304
|
-
},
|
|
327
|
+
() => type.decoder(input),
|
|
328
|
+
() => makeErrorText({ long, label, type }),
|
|
305
329
|
);
|
|
306
330
|
}
|
|
307
331
|
|
|
308
|
-
function
|
|
332
|
+
function makeErrorText(params: {
|
|
333
|
+
long: string;
|
|
334
|
+
label: string | undefined;
|
|
335
|
+
type: Type<any>;
|
|
336
|
+
}): TypoText {
|
|
337
|
+
const errorText = new TypoText();
|
|
338
|
+
errorText.push(new TypoString(`--${params.long}`, typoStyleConstants));
|
|
339
|
+
if (params.label !== undefined) {
|
|
340
|
+
errorText.push(new TypoString(`: `));
|
|
341
|
+
errorText.push(new TypoString(params.label, typoStyleUserInput));
|
|
342
|
+
} else {
|
|
343
|
+
errorText.push(new TypoString(`: `));
|
|
344
|
+
errorText.push(new TypoString(params.type.content, typoStyleLogic));
|
|
345
|
+
}
|
|
346
|
+
return errorText;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function setupOptionAliased(
|
|
309
350
|
readerOptions: ReaderOptions,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
351
|
+
params: {
|
|
352
|
+
longKey: string;
|
|
353
|
+
shortKey: string | undefined;
|
|
354
|
+
aliasLongKeys: Array<string> | undefined;
|
|
355
|
+
aliasShortKeys: Array<string> | undefined;
|
|
356
|
+
restGuard: ReaderOptionRestGuard;
|
|
357
|
+
nextGuard: ReaderOptionNextGuard;
|
|
316
358
|
},
|
|
317
|
-
) {
|
|
318
|
-
const {
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
|
|
359
|
+
): () => Array<{ identifier: string; value: ReaderOptionValue }> {
|
|
360
|
+
const { longKey, shortKey, aliasLongKeys, aliasShortKeys } = params;
|
|
361
|
+
const longKeys = [longKey];
|
|
362
|
+
if (aliasLongKeys !== undefined) {
|
|
363
|
+
longKeys.push(...aliasLongKeys);
|
|
322
364
|
}
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
365
|
+
const shortKeys = shortKey ? [shortKey] : [];
|
|
366
|
+
if (aliasShortKeys !== undefined) {
|
|
367
|
+
shortKeys.push(...aliasShortKeys);
|
|
326
368
|
}
|
|
327
|
-
return readerOptions
|
|
369
|
+
return setupOptionMany(readerOptions, {
|
|
370
|
+
longKeys,
|
|
371
|
+
shortKeys,
|
|
372
|
+
restGuard: params.restGuard,
|
|
373
|
+
nextGuard: params.nextGuard,
|
|
374
|
+
});
|
|
328
375
|
}
|
|
329
376
|
|
|
330
|
-
function
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
377
|
+
function setupOptionMany(
|
|
378
|
+
readerOptions: ReaderOptions,
|
|
379
|
+
params: {
|
|
380
|
+
longKeys: Array<string>;
|
|
381
|
+
shortKeys: Array<string>;
|
|
382
|
+
restGuard: ReaderOptionRestGuard;
|
|
383
|
+
nextGuard: ReaderOptionNextGuard;
|
|
384
|
+
},
|
|
385
|
+
): () => Array<{ identifier: string; value: ReaderOptionValue }> {
|
|
386
|
+
const { longKeys, shortKeys, restGuard, nextGuard } = params;
|
|
387
|
+
const getters = new Array<ReaderOptionGetter>();
|
|
388
|
+
for (const key of longKeys) {
|
|
389
|
+
getters.push(readerOptions.registerOptionLong({ key, nextGuard }));
|
|
390
|
+
}
|
|
391
|
+
for (const key of shortKeys) {
|
|
392
|
+
getters.push(
|
|
393
|
+
readerOptions.registerOptionShort({ key, restGuard, nextGuard }),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return () => {
|
|
397
|
+
const results = new Array();
|
|
398
|
+
for (const getter of getters) {
|
|
399
|
+
const { identifier, values } = getter();
|
|
400
|
+
for (const value of values) {
|
|
401
|
+
results.push({ identifier, value });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return results;
|
|
405
|
+
};
|
|
337
406
|
}
|
|
338
407
|
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
context: string,
|
|
343
|
-
): never {
|
|
344
|
-
throw new TypoError(
|
|
345
|
-
new TypoText(
|
|
346
|
-
new TypoString(`--${long}`, typoStyleConstants),
|
|
347
|
-
new TypoString(`: Failed to get default value (${context})`),
|
|
348
|
-
),
|
|
349
|
-
error,
|
|
408
|
+
function throwSetMultipleTimesError(identifiers: Array<string>): never {
|
|
409
|
+
const identifiersTexts = Array.from(new Set(identifiers)).map(
|
|
410
|
+
(identifier) => new TypoString(identifier, typoStyleConstants),
|
|
350
411
|
);
|
|
412
|
+
const errorText = new TypoText();
|
|
413
|
+
errorText.pushJoined(identifiersTexts, new TypoString(", "), 3);
|
|
414
|
+
errorText.push(new TypoString(`: Must not be set multiple times.`));
|
|
415
|
+
throw new TypoError(errorText);
|
|
351
416
|
}
|
package/src/lib/Positional.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ReaderPositionals } from "./Reader";
|
|
2
2
|
import { Type } from "./Type";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
TypoError,
|
|
5
|
+
TypoString,
|
|
6
|
+
typoStyleRegularWeaker,
|
|
7
|
+
typoStyleUserInput,
|
|
8
|
+
TypoText,
|
|
9
|
+
} from "./Typo";
|
|
4
10
|
import { UsagePositional } from "./Usage";
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -32,19 +38,25 @@ export type PositionalDecoder<Value> = {
|
|
|
32
38
|
/**
|
|
33
39
|
* Returns the decoded positional value.
|
|
34
40
|
*
|
|
35
|
-
* @throws
|
|
41
|
+
* @throws if decoding failed.
|
|
36
42
|
*/
|
|
37
43
|
decodeValue(): Value;
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
|
-
* Creates a required positional — missing token throws
|
|
47
|
+
* Creates a required positional — missing token throws.
|
|
48
|
+
*
|
|
49
|
+
* Syntax: `<type>`, e.g. `<NAME>`.
|
|
50
|
+
* Parsing logic:
|
|
51
|
+
* - "token" → decoded with `type.decoder("token")`
|
|
52
|
+
* - token missing → throws
|
|
42
53
|
*
|
|
43
54
|
* @typeParam Value - Type produced by the decoder.
|
|
44
55
|
*
|
|
45
56
|
* @param definition.description - Help text.
|
|
46
57
|
* @param definition.hint - Short note shown in parentheses.
|
|
47
58
|
* @param definition.type - Decoder for the raw token.
|
|
59
|
+
* @param definition.missing - Message shown when the token is missing.
|
|
48
60
|
* @returns A {@link Positional}`<Value>`.
|
|
49
61
|
*
|
|
50
62
|
* @example
|
|
@@ -53,8 +65,6 @@ export type PositionalDecoder<Value> = {
|
|
|
53
65
|
* type: type("name"),
|
|
54
66
|
* description: "The name to greet",
|
|
55
67
|
* });
|
|
56
|
-
* // Usage:
|
|
57
|
-
* // my-cli Alice → "Alice"
|
|
58
68
|
* ```
|
|
59
69
|
*/
|
|
60
70
|
export function positionalRequired<Value>(definition: {
|
|
@@ -71,12 +81,15 @@ export function positionalRequired<Value>(definition: {
|
|
|
71
81
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
72
82
|
const positional = readerPositionals.consumePositional();
|
|
73
83
|
if (positional === undefined) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
const errorText = makeErrorText(label);
|
|
85
|
+
errorText.push(new TypoString(`: Is required, but was not provided.`));
|
|
86
|
+
if (description !== undefined) {
|
|
87
|
+
// TODO - should there be a dedicated hint here ?
|
|
88
|
+
errorText.push(
|
|
89
|
+
new TypoString(` (${description})`, typoStyleRegularWeaker),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw new TypoError(errorText);
|
|
80
93
|
}
|
|
81
94
|
return {
|
|
82
95
|
decodeValue() {
|
|
@@ -90,6 +103,11 @@ export function positionalRequired<Value>(definition: {
|
|
|
90
103
|
/**
|
|
91
104
|
* Creates an optional positional — absent token falls back to `default()`.
|
|
92
105
|
*
|
|
106
|
+
* Syntax: `[type]`, e.g. `[NAME]`.
|
|
107
|
+
* Parsing logic:
|
|
108
|
+
* - "token" → decoded with `type.decoder("token")`
|
|
109
|
+
* - token missing → `default()`
|
|
110
|
+
*
|
|
93
111
|
* @typeParam Value - Type produced by the decoder (or the default).
|
|
94
112
|
*
|
|
95
113
|
* @param definition.description - Help text.
|
|
@@ -102,12 +120,10 @@ export function positionalRequired<Value>(definition: {
|
|
|
102
120
|
* ```ts
|
|
103
121
|
* const greeteePositional = positionalOptional({
|
|
104
122
|
* type: type("name"),
|
|
105
|
-
* description: "Name to greet
|
|
123
|
+
* description: "Name to greet",
|
|
124
|
+
* hint: "Defaults to \"world\"",
|
|
106
125
|
* default: () => "world",
|
|
107
126
|
* });
|
|
108
|
-
* // Usage:
|
|
109
|
-
* // my-cli → "world"
|
|
110
|
-
* // my-cli Alice → "Alice"
|
|
111
127
|
* ```
|
|
112
128
|
*/
|
|
113
129
|
export function positionalOptional<Value>(definition: {
|
|
@@ -144,6 +160,12 @@ export function positionalOptional<Value>(definition: {
|
|
|
144
160
|
* Creates a variadic positional that collects zero or more remaining tokens into an array.
|
|
145
161
|
* Optionally stops at `endDelimiter` (consumed, not included).
|
|
146
162
|
*
|
|
163
|
+
* Syntax: `[type]...`, e.g. `[NAME]...`.
|
|
164
|
+
* Parsing logic:
|
|
165
|
+
* - "a b ..." → decoded with `[type.decoder("a")`, `type.decoder("b"), ...]``
|
|
166
|
+
* - token missing → stops collection
|
|
167
|
+
* - endDelimiter encountered → stops collection
|
|
168
|
+
*
|
|
147
169
|
* @typeParam Value - Type produced by the decoder for each token.
|
|
148
170
|
*
|
|
149
171
|
* @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
|
|
@@ -158,9 +180,6 @@ export function positionalOptional<Value>(definition: {
|
|
|
158
180
|
* type: typePath(),
|
|
159
181
|
* description: "Files to process",
|
|
160
182
|
* });
|
|
161
|
-
* // Usage:
|
|
162
|
-
* // my-cli → []
|
|
163
|
-
* // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
|
|
164
183
|
* ```
|
|
165
184
|
*/
|
|
166
185
|
export function positionalVariadics<Value>(definition: {
|
|
@@ -212,11 +231,14 @@ function decodeValue<Value>(
|
|
|
212
231
|
);
|
|
213
232
|
}
|
|
214
233
|
|
|
234
|
+
function makeErrorText(label: string): TypoText {
|
|
235
|
+
const errorText = new TypoText();
|
|
236
|
+
errorText.push(new TypoString(label, typoStyleUserInput));
|
|
237
|
+
return errorText;
|
|
238
|
+
}
|
|
239
|
+
|
|
215
240
|
function throwsWhenFailedToGetDefault(label: string): never {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
new TypoString(`: Failed to get default value`),
|
|
220
|
-
),
|
|
221
|
-
);
|
|
241
|
+
const errorText = makeErrorText(label);
|
|
242
|
+
errorText.push(new TypoString(`: Failed to get default value.`));
|
|
243
|
+
throw new TypoError(errorText);
|
|
222
244
|
}
|