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