cli-kiss 0.2.4 → 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/dist/index.d.ts +150 -164
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -0
- package/docs/guide/01_getting_started.md +12 -13
- package/docs/guide/02_commands.md +12 -29
- package/docs/guide/03_options.md +16 -25
- 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/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 +14 -35
- package/src/lib/Operation.ts +13 -4
- package/src/lib/Option.ts +118 -162
- package/src/lib/Positional.ts +37 -62
- package/src/lib/Reader.ts +3 -3
- package/src/lib/Run.ts +74 -46
- package/src/lib/Type.ts +227 -141
- package/src/lib/Typo.ts +36 -24
- package/src/lib/Usage.ts +27 -42
- package/tests/unit.Reader.parsings.ts +50 -0
- package/tests/unit.command.execute.ts +13 -13
- package/tests/unit.command.usage.ts +60 -54
- package/tests/unit.runner.colors.ts +197 -0
- package/tests/unit.runner.cycle.ts +69 -55
- package/tests/unit.runner.errors.ts +12 -20
package/src/lib/Option.ts
CHANGED
|
@@ -44,15 +44,15 @@ export type OptionDecoder<Value> = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
|
|
46
46
|
*
|
|
47
|
-
* Parsing: absent →
|
|
48
|
-
* specified more than once → {@link TypoError}.
|
|
47
|
+
* Parsing: absent → default value; `--flag` / `--flag=yes` → `true`; `--flag=no` → `false`;
|
|
48
|
+
* specified more than once → throws {@link TypoError}.
|
|
49
49
|
*
|
|
50
50
|
* @param definition.long - Long-form name (without `--`).
|
|
51
51
|
* @param definition.short - Short-form name (without `-`).
|
|
52
52
|
* @param definition.description - Help text.
|
|
53
53
|
* @param definition.hint - Short note shown in parentheses.
|
|
54
54
|
* @param definition.aliases - Additional names.
|
|
55
|
-
* @param definition.default - Default when absent.
|
|
55
|
+
* @param definition.default - Default value when absent.
|
|
56
56
|
* @returns An {@link Option}`<boolean>`.
|
|
57
57
|
*
|
|
58
58
|
* @example
|
|
@@ -62,102 +62,50 @@ export type OptionDecoder<Value> = {
|
|
|
62
62
|
* short: "v",
|
|
63
63
|
* description: "Enable verbose output",
|
|
64
64
|
* });
|
|
65
|
+
* // Usage:
|
|
66
|
+
* // my-cli → false
|
|
67
|
+
* // my-cli --verbose → true
|
|
68
|
+
* // my-cli --verbose=yes → true
|
|
69
|
+
* // my-cli -v=no → false
|
|
65
70
|
* ```
|
|
66
71
|
*/
|
|
67
72
|
export function optionFlag(definition: {
|
|
68
|
-
long:
|
|
73
|
+
long: string;
|
|
69
74
|
short?: string;
|
|
70
75
|
description?: string;
|
|
71
76
|
hint?: string;
|
|
72
|
-
aliases?: { longs?: Array<
|
|
77
|
+
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
73
78
|
default?: boolean;
|
|
74
79
|
}): Option<boolean> {
|
|
75
|
-
const
|
|
80
|
+
const type = typeBoolean("value");
|
|
81
|
+
const { long, short, description, hint, aliases } = definition;
|
|
76
82
|
return {
|
|
77
83
|
generateUsage() {
|
|
78
|
-
return {
|
|
79
|
-
short: definition.short,
|
|
80
|
-
long: definition.long,
|
|
81
|
-
label: undefined,
|
|
82
|
-
annotation: "[=no]",
|
|
83
|
-
description: definition.description,
|
|
84
|
-
hint: definition.hint,
|
|
85
|
-
};
|
|
84
|
+
return { short, long, annotation: "[=no]", description, hint };
|
|
86
85
|
},
|
|
87
86
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
long: longNegative,
|
|
94
|
-
short: undefined,
|
|
95
|
-
aliasesShorts: undefined,
|
|
96
|
-
aliasesLongs: aliasesLongsNegatives,
|
|
97
|
-
parsing: { consumeShortGroup: false, consumeNextArg: () => false },
|
|
98
|
-
});
|
|
99
|
-
const keyPositive = registerOption(readerOptions, {
|
|
100
|
-
long: definition.long,
|
|
101
|
-
short: definition.short,
|
|
102
|
-
aliasesLongs: definition.aliases?.longs,
|
|
103
|
-
aliasesShorts: definition.aliases?.shorts,
|
|
87
|
+
const key = registerOption(readerOptions, {
|
|
88
|
+
long,
|
|
89
|
+
short,
|
|
90
|
+
aliasesLongs: aliases?.longs,
|
|
91
|
+
aliasesShorts: aliases?.shorts,
|
|
104
92
|
parsing: { consumeShortGroup: false, consumeNextArg: () => false },
|
|
105
93
|
});
|
|
106
94
|
return {
|
|
107
95
|
getAndDecodeValue() {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
throw new TypoError(
|
|
112
|
-
new TypoText(
|
|
113
|
-
new TypoString(`--${definition.long}`, typoStyleConstants),
|
|
114
|
-
new TypoString(`: Must not be set multiple times`),
|
|
115
|
-
),
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
if (negativeResults.length > 1) {
|
|
119
|
-
throw new TypoError(
|
|
120
|
-
new TypoText(
|
|
121
|
-
new TypoString(`--${longNegative}`, typoStyleConstants),
|
|
122
|
-
new TypoString(`: Must not be set multiple times`),
|
|
123
|
-
),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (negativeResults.length > 0 && positiveResults.length > 0) {
|
|
127
|
-
throw new TypoError(
|
|
128
|
-
new TypoText(
|
|
129
|
-
new TypoString(`--${definition.long}`, typoStyleConstants),
|
|
130
|
-
new TypoString(`: Must not be set in combination with: `),
|
|
131
|
-
new TypoString(`--${longNegative}`, typoStyleConstants),
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
if (negativeResults.length > 0) {
|
|
136
|
-
const negativeResult = negativeResults[0]!;
|
|
137
|
-
if (negativeResult.inlined) {
|
|
138
|
-
throw new TypoError(
|
|
139
|
-
new TypoText(
|
|
140
|
-
new TypoString(`--${longNegative}`, typoStyleConstants),
|
|
141
|
-
new TypoString(`: Must not have a value`),
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
96
|
+
const optionResults = readerOptions.getOptionValues(key);
|
|
97
|
+
if (optionResults.length > 1) {
|
|
98
|
+
throwSetMultipleTimesError(long);
|
|
146
99
|
}
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
short: definition.short,
|
|
152
|
-
label,
|
|
153
|
-
type: typeBoolean,
|
|
154
|
-
input:
|
|
155
|
-
positiveResult.inlined === null
|
|
156
|
-
? "true"
|
|
157
|
-
: positiveResult.inlined,
|
|
158
|
-
});
|
|
100
|
+
if (optionResults.length === 0) {
|
|
101
|
+
return definition.default === undefined
|
|
102
|
+
? false
|
|
103
|
+
: definition.default;
|
|
159
104
|
}
|
|
160
|
-
|
|
105
|
+
const positiveResult = optionResults[0]!;
|
|
106
|
+
const value =
|
|
107
|
+
positiveResult.inlined === null ? "true" : positiveResult.inlined;
|
|
108
|
+
return decodeValue({ long, short, type, input: value });
|
|
161
109
|
},
|
|
162
110
|
};
|
|
163
111
|
},
|
|
@@ -167,7 +115,7 @@ export function optionFlag(definition: {
|
|
|
167
115
|
/**
|
|
168
116
|
* Creates an option that accepts exactly one value (e.g. `--output dist/`).
|
|
169
117
|
*
|
|
170
|
-
* Parsing: absent → `
|
|
118
|
+
* Parsing: absent → `defaultValue()`; once → decoded with `type`; more than once → {@link TypoError}.
|
|
171
119
|
* Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
|
|
172
120
|
*
|
|
173
121
|
* @typeParam Value - Type produced by the decoder.
|
|
@@ -177,9 +125,9 @@ export function optionFlag(definition: {
|
|
|
177
125
|
* @param definition.description - Help text.
|
|
178
126
|
* @param definition.hint - Short note shown in parentheses.
|
|
179
127
|
* @param definition.aliases - Additional names.
|
|
180
|
-
* @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
|
|
181
128
|
* @param definition.type - Decoder for the raw string value.
|
|
182
|
-
* @param definition.
|
|
129
|
+
* @param definition.valueWhenNotDefined - Default value when the option is not specified at all.
|
|
130
|
+
* @param definition.valueWhenNotInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
|
|
183
131
|
* @returns An {@link Option}`<Value>`.
|
|
184
132
|
*
|
|
185
133
|
* @example
|
|
@@ -187,79 +135,75 @@ export function optionFlag(definition: {
|
|
|
187
135
|
* const outputOption = optionSingleValue({
|
|
188
136
|
* long: "output",
|
|
189
137
|
* short: "o",
|
|
190
|
-
* type:
|
|
191
|
-
* label: "PATH",
|
|
138
|
+
* type: typePath(),
|
|
192
139
|
* description: "Output directory",
|
|
193
|
-
*
|
|
140
|
+
* valueWhenNotDefined: () => "dist",
|
|
194
141
|
* });
|
|
142
|
+
* // Usage:
|
|
143
|
+
* // my-cli → "dist"
|
|
144
|
+
* // my-cli --output folder → "folder"
|
|
145
|
+
* // my-cli -o folder → "folder"
|
|
195
146
|
* ```
|
|
196
147
|
*/
|
|
197
148
|
export function optionSingleValue<Value>(definition: {
|
|
198
|
-
long:
|
|
149
|
+
long: string;
|
|
199
150
|
short?: string;
|
|
200
151
|
description?: string;
|
|
201
152
|
hint?: string;
|
|
202
|
-
aliases?: { longs?: Array<
|
|
203
|
-
label?: Uppercase<string>;
|
|
153
|
+
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
204
154
|
type: Type<Value>;
|
|
205
|
-
|
|
155
|
+
defaultWhenNotDefined: () => Value;
|
|
156
|
+
defaultWhenNotInlined?: () => Value;
|
|
206
157
|
}): Option<Value> {
|
|
207
|
-
const
|
|
158
|
+
const { long, short, description, hint, aliases, type } = definition;
|
|
159
|
+
const label = `<${type.content}>`;
|
|
208
160
|
return {
|
|
209
161
|
generateUsage() {
|
|
210
|
-
return {
|
|
211
|
-
short: definition.short,
|
|
212
|
-
long: definition.long,
|
|
213
|
-
label: label as Uppercase<string>,
|
|
214
|
-
annotation: undefined,
|
|
215
|
-
description: definition.description,
|
|
216
|
-
hint: definition.hint,
|
|
217
|
-
};
|
|
162
|
+
return { short, long, label, description, hint };
|
|
218
163
|
},
|
|
219
164
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
220
165
|
const key = registerOption(readerOptions, {
|
|
221
|
-
long
|
|
222
|
-
short
|
|
223
|
-
aliasesLongs:
|
|
224
|
-
aliasesShorts:
|
|
166
|
+
long,
|
|
167
|
+
short,
|
|
168
|
+
aliasesLongs: aliases?.longs,
|
|
169
|
+
aliasesShorts: aliases?.shorts,
|
|
225
170
|
parsing: {
|
|
226
171
|
consumeShortGroup: true,
|
|
227
|
-
consumeNextArg
|
|
228
|
-
|
|
172
|
+
consumeNextArg(inlined, separated) {
|
|
173
|
+
if (definition.defaultWhenNotInlined !== undefined) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return inlined === null && separated.length === 0;
|
|
177
|
+
},
|
|
229
178
|
},
|
|
230
179
|
});
|
|
231
180
|
return {
|
|
232
181
|
getAndDecodeValue() {
|
|
233
182
|
const optionResults = readerOptions.getOptionValues(key);
|
|
234
183
|
if (optionResults.length > 1) {
|
|
235
|
-
|
|
236
|
-
new TypoText(
|
|
237
|
-
new TypoString(`--${definition.long}`, typoStyleConstants),
|
|
238
|
-
new TypoString(`: Requires a single value, but got multiple`),
|
|
239
|
-
),
|
|
240
|
-
);
|
|
184
|
+
throwSetMultipleTimesError(long);
|
|
241
185
|
}
|
|
242
186
|
const optionResult = optionResults[0];
|
|
243
187
|
if (optionResult === undefined) {
|
|
244
188
|
try {
|
|
245
|
-
return definition.
|
|
189
|
+
return definition.defaultWhenNotDefined();
|
|
246
190
|
} catch (error) {
|
|
247
|
-
|
|
248
|
-
new TypoText(
|
|
249
|
-
new TypoString(`--${definition.long}`, typoStyleConstants),
|
|
250
|
-
new TypoString(`: Failed to get default value`),
|
|
251
|
-
),
|
|
252
|
-
error,
|
|
253
|
-
);
|
|
191
|
+
throwFailedToGetDefaultValueError(long, error, "not set");
|
|
254
192
|
}
|
|
255
193
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
short:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
194
|
+
if (optionResult.inlined) {
|
|
195
|
+
const inlined = optionResult.inlined;
|
|
196
|
+
return decodeValue({ long, short, label, type, input: inlined });
|
|
197
|
+
}
|
|
198
|
+
if (definition.defaultWhenNotInlined !== undefined) {
|
|
199
|
+
try {
|
|
200
|
+
return definition.defaultWhenNotInlined();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throwFailedToGetDefaultValueError(long, error, "not inlined");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const separated = optionResult.separated[0]!;
|
|
206
|
+
return decodeValue({ long, short, label, type, input: separated });
|
|
263
207
|
},
|
|
264
208
|
};
|
|
265
209
|
},
|
|
@@ -279,7 +223,6 @@ export function optionSingleValue<Value>(definition: {
|
|
|
279
223
|
* @param definition.description - Help text.
|
|
280
224
|
* @param definition.hint - Short note shown in parentheses.
|
|
281
225
|
* @param definition.aliases - Additional names.
|
|
282
|
-
* @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
|
|
283
226
|
* @param definition.type - Decoder applied to each raw string value.
|
|
284
227
|
* @returns An {@link Option}`<Array<Value>>`.
|
|
285
228
|
*
|
|
@@ -296,33 +239,25 @@ export function optionSingleValue<Value>(definition: {
|
|
|
296
239
|
* ```
|
|
297
240
|
*/
|
|
298
241
|
export function optionRepeatable<Value>(definition: {
|
|
299
|
-
long:
|
|
242
|
+
long: string;
|
|
300
243
|
short?: string;
|
|
301
244
|
description?: string;
|
|
302
245
|
hint?: string;
|
|
303
|
-
aliases?: { longs?: Array<
|
|
304
|
-
label?: Uppercase<string>;
|
|
246
|
+
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
305
247
|
type: Type<Value>;
|
|
306
248
|
}): Option<Array<Value>> {
|
|
307
|
-
const
|
|
249
|
+
const { long, short, description, hint, aliases, type } = definition;
|
|
250
|
+
const label = `<${type.content}>`;
|
|
308
251
|
return {
|
|
309
252
|
generateUsage() {
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
short: definition.short,
|
|
313
|
-
long: definition.long,
|
|
314
|
-
label: label as Uppercase<string>,
|
|
315
|
-
annotation: " [*]",
|
|
316
|
-
description: definition.description,
|
|
317
|
-
hint: definition.hint,
|
|
318
|
-
};
|
|
253
|
+
return { short, long, label, annotation: " [*]", description, hint };
|
|
319
254
|
},
|
|
320
255
|
registerAndMakeDecoder(readerOptions: ReaderOptions) {
|
|
321
256
|
const key = registerOption(readerOptions, {
|
|
322
|
-
long
|
|
323
|
-
short
|
|
324
|
-
aliasesLongs:
|
|
325
|
-
aliasesShorts:
|
|
257
|
+
long,
|
|
258
|
+
short,
|
|
259
|
+
aliasesLongs: aliases?.longs,
|
|
260
|
+
aliasesShorts: aliases?.shorts,
|
|
326
261
|
parsing: {
|
|
327
262
|
consumeShortGroup: true,
|
|
328
263
|
consumeNextArg: (inlined, separated) =>
|
|
@@ -332,15 +267,10 @@ export function optionRepeatable<Value>(definition: {
|
|
|
332
267
|
return {
|
|
333
268
|
getAndDecodeValue() {
|
|
334
269
|
const optionResults = readerOptions.getOptionValues(key);
|
|
335
|
-
return optionResults.map((optionResult) =>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
label,
|
|
340
|
-
type: definition.type,
|
|
341
|
-
input: optionResult.inlined ?? optionResult.separated[0]!,
|
|
342
|
-
}),
|
|
343
|
-
);
|
|
270
|
+
return optionResults.map((optionResult) => {
|
|
271
|
+
const input = optionResult.inlined ?? optionResult.separated[0]!;
|
|
272
|
+
return decodeValue({ long, short, label, type, input });
|
|
273
|
+
});
|
|
344
274
|
},
|
|
345
275
|
};
|
|
346
276
|
},
|
|
@@ -349,8 +279,8 @@ export function optionRepeatable<Value>(definition: {
|
|
|
349
279
|
|
|
350
280
|
function decodeValue<Value>(params: {
|
|
351
281
|
long: string;
|
|
352
|
-
short
|
|
353
|
-
label
|
|
282
|
+
short?: string | undefined;
|
|
283
|
+
label?: string | undefined;
|
|
354
284
|
type: Type<Value>;
|
|
355
285
|
input: string;
|
|
356
286
|
}): Value {
|
|
@@ -363,10 +293,13 @@ function decodeValue<Value>(params: {
|
|
|
363
293
|
text.push(new TypoString(`, `));
|
|
364
294
|
}
|
|
365
295
|
text.push(new TypoString(`--${params.long}`, typoStyleConstants));
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
303
|
return text;
|
|
371
304
|
},
|
|
372
305
|
);
|
|
@@ -375,9 +308,9 @@ function decodeValue<Value>(params: {
|
|
|
375
308
|
function registerOption(
|
|
376
309
|
readerOptions: ReaderOptions,
|
|
377
310
|
definition: {
|
|
378
|
-
long:
|
|
311
|
+
long: string;
|
|
379
312
|
short: undefined | string;
|
|
380
|
-
aliasesLongs: undefined | Array<
|
|
313
|
+
aliasesLongs: undefined | Array<string>;
|
|
381
314
|
aliasesShorts: undefined | Array<string>;
|
|
382
315
|
parsing: ReaderOptionParsing;
|
|
383
316
|
},
|
|
@@ -393,3 +326,26 @@ function registerOption(
|
|
|
393
326
|
}
|
|
394
327
|
return readerOptions.registerOption({ longs, shorts, parsing });
|
|
395
328
|
}
|
|
329
|
+
|
|
330
|
+
function throwSetMultipleTimesError(long: string): never {
|
|
331
|
+
throw new TypoError(
|
|
332
|
+
new TypoText(
|
|
333
|
+
new TypoString(`--${long}`, typoStyleConstants),
|
|
334
|
+
new TypoString(`: Must not be set multiple times`),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function throwFailedToGetDefaultValueError(
|
|
340
|
+
long: string,
|
|
341
|
+
error: unknown,
|
|
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,
|
|
350
|
+
);
|
|
351
|
+
}
|
package/src/lib/Positional.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { ReaderPositionals } from "./Reader";
|
|
2
2
|
import { Type } from "./Type";
|
|
3
|
-
import {
|
|
4
|
-
TypoError,
|
|
5
|
-
TypoString,
|
|
6
|
-
typoStyleLogic,
|
|
7
|
-
typoStyleUserInput,
|
|
8
|
-
TypoText,
|
|
9
|
-
} from "./Typo";
|
|
3
|
+
import { TypoError, TypoString, typoStyleUserInput, TypoText } from "./Typo";
|
|
10
4
|
import { UsagePositional } from "./Usage";
|
|
11
5
|
|
|
12
6
|
/**
|
|
@@ -45,40 +39,34 @@ export type PositionalDecoder<Value> = {
|
|
|
45
39
|
|
|
46
40
|
/**
|
|
47
41
|
* Creates a required positional — missing token throws {@link TypoError}.
|
|
48
|
-
* Label defaults to uppercased `type.content` in angle brackets (e.g. `<STRING>`).
|
|
49
42
|
*
|
|
50
43
|
* @typeParam Value - Type produced by the decoder.
|
|
51
44
|
*
|
|
52
45
|
* @param definition.description - Help text.
|
|
53
46
|
* @param definition.hint - Short note shown in parentheses.
|
|
54
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
55
47
|
* @param definition.type - Decoder for the raw token.
|
|
56
48
|
* @returns A {@link Positional}`<Value>`.
|
|
57
49
|
*
|
|
58
50
|
* @example
|
|
59
51
|
* ```ts
|
|
60
52
|
* const namePositional = positionalRequired({
|
|
61
|
-
* type:
|
|
62
|
-
* label: "NAME",
|
|
53
|
+
* type: type("name"),
|
|
63
54
|
* description: "The name to greet",
|
|
64
55
|
* });
|
|
65
|
-
* //
|
|
56
|
+
* // Usage:
|
|
57
|
+
* // my-cli Alice → "Alice"
|
|
66
58
|
* ```
|
|
67
59
|
*/
|
|
68
60
|
export function positionalRequired<Value>(definition: {
|
|
69
61
|
description?: string;
|
|
70
62
|
hint?: string;
|
|
71
|
-
label?: Uppercase<string>;
|
|
72
63
|
type: Type<Value>;
|
|
73
64
|
}): Positional<Value> {
|
|
74
|
-
const
|
|
65
|
+
const { description, hint, type } = definition;
|
|
66
|
+
const label = `<${type.content}>`;
|
|
75
67
|
return {
|
|
76
68
|
generateUsage() {
|
|
77
|
-
return {
|
|
78
|
-
description: definition.description,
|
|
79
|
-
hint: definition.hint,
|
|
80
|
-
label: label as Uppercase<string>,
|
|
81
|
-
};
|
|
69
|
+
return { description, hint, label };
|
|
82
70
|
},
|
|
83
71
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
84
72
|
const positional = readerPositionals.consumePositional();
|
|
@@ -101,13 +89,11 @@ export function positionalRequired<Value>(definition: {
|
|
|
101
89
|
|
|
102
90
|
/**
|
|
103
91
|
* Creates an optional positional — absent token falls back to `default()`.
|
|
104
|
-
* Label defaults to uppercased `type.content` in square brackets (e.g. `[STRING]`).
|
|
105
92
|
*
|
|
106
93
|
* @typeParam Value - Type produced by the decoder (or the default).
|
|
107
94
|
*
|
|
108
95
|
* @param definition.description - Help text.
|
|
109
96
|
* @param definition.hint - Short note shown in parentheses.
|
|
110
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
111
97
|
* @param definition.type - Decoder for the raw token.
|
|
112
98
|
* @param definition.default - Value when absent. Throw to make it required.
|
|
113
99
|
* @returns A {@link Positional}`<Value>`.
|
|
@@ -115,30 +101,26 @@ export function positionalRequired<Value>(definition: {
|
|
|
115
101
|
* @example
|
|
116
102
|
* ```ts
|
|
117
103
|
* const greeteePositional = positionalOptional({
|
|
118
|
-
* type:
|
|
119
|
-
* label: "NAME",
|
|
104
|
+
* type: type("name"),
|
|
120
105
|
* description: "Name to greet (default: world)",
|
|
121
106
|
* default: () => "world",
|
|
122
107
|
* });
|
|
123
|
-
* //
|
|
124
|
-
* //
|
|
108
|
+
* // Usage:
|
|
109
|
+
* // my-cli → "world"
|
|
110
|
+
* // my-cli Alice → "Alice"
|
|
125
111
|
* ```
|
|
126
112
|
*/
|
|
127
113
|
export function positionalOptional<Value>(definition: {
|
|
128
114
|
description?: string;
|
|
129
115
|
hint?: string;
|
|
130
|
-
label?: Uppercase<string>;
|
|
131
116
|
type: Type<Value>;
|
|
132
117
|
default: () => Value;
|
|
133
118
|
}): Positional<Value> {
|
|
134
|
-
const
|
|
119
|
+
const { description, hint, type } = definition;
|
|
120
|
+
const label = `[${type.content}]`;
|
|
135
121
|
return {
|
|
136
122
|
generateUsage() {
|
|
137
|
-
return {
|
|
138
|
-
description: definition.description,
|
|
139
|
-
hint: definition.hint,
|
|
140
|
-
label: label as Uppercase<string>,
|
|
141
|
-
};
|
|
123
|
+
return { description, hint, label };
|
|
142
124
|
},
|
|
143
125
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
144
126
|
const positional = readerPositionals.consumePositional();
|
|
@@ -148,13 +130,7 @@ export function positionalOptional<Value>(definition: {
|
|
|
148
130
|
try {
|
|
149
131
|
return definition.default();
|
|
150
132
|
} catch (error) {
|
|
151
|
-
|
|
152
|
-
new TypoText(
|
|
153
|
-
new TypoString(label, typoStyleUserInput),
|
|
154
|
-
new TypoString(`: Failed to get default value`),
|
|
155
|
-
),
|
|
156
|
-
error,
|
|
157
|
-
);
|
|
133
|
+
throwsWhenFailedToGetDefault(label);
|
|
158
134
|
}
|
|
159
135
|
}
|
|
160
136
|
return decodeValue(label, definition.type, positional);
|
|
@@ -166,46 +142,41 @@ export function positionalOptional<Value>(definition: {
|
|
|
166
142
|
|
|
167
143
|
/**
|
|
168
144
|
* Creates a variadic positional that collects zero or more remaining tokens into an array.
|
|
169
|
-
*
|
|
145
|
+
* Optionally stops at `endDelimiter` (consumed, not included).
|
|
170
146
|
*
|
|
171
147
|
* @typeParam Value - Type produced by the decoder for each token.
|
|
172
148
|
*
|
|
173
149
|
* @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
|
|
174
150
|
* @param definition.description - Help text.
|
|
175
151
|
* @param definition.hint - Short note shown in parentheses.
|
|
176
|
-
* @param definition.label - Label without brackets; defaults to uppercased `type.content`.
|
|
177
152
|
* @param definition.type - Decoder applied to each token.
|
|
178
153
|
* @returns A {@link Positional}`<Array<Value>>`.
|
|
179
154
|
*
|
|
180
155
|
* @example
|
|
181
156
|
* ```ts
|
|
182
157
|
* const filesPositional = positionalVariadics({
|
|
183
|
-
* type:
|
|
184
|
-
* label: "FILE",
|
|
158
|
+
* type: typePath(),
|
|
185
159
|
* description: "Files to process",
|
|
186
160
|
* });
|
|
187
|
-
* //
|
|
188
|
-
* //
|
|
161
|
+
* // Usage:
|
|
162
|
+
* // my-cli → []
|
|
163
|
+
* // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
|
|
189
164
|
* ```
|
|
190
165
|
*/
|
|
191
166
|
export function positionalVariadics<Value>(definition: {
|
|
192
167
|
endDelimiter?: string;
|
|
193
168
|
description?: string;
|
|
194
169
|
hint?: string;
|
|
195
|
-
label?: Uppercase<string>;
|
|
196
170
|
type: Type<Value>;
|
|
197
171
|
}): Positional<Array<Value>> {
|
|
198
|
-
const
|
|
172
|
+
const { description, hint, type } = definition;
|
|
173
|
+
const labelSingle = `[${type.content}]`;
|
|
174
|
+
const labelMultiple =
|
|
175
|
+
`${labelSingle}...` +
|
|
176
|
+
(definition.endDelimiter ? ` ["${definition.endDelimiter}"]` : "");
|
|
199
177
|
return {
|
|
200
178
|
generateUsage() {
|
|
201
|
-
return {
|
|
202
|
-
description: definition.description,
|
|
203
|
-
hint: definition.hint,
|
|
204
|
-
label: (`${label}...` +
|
|
205
|
-
(definition.endDelimiter
|
|
206
|
-
? ` ["${definition.endDelimiter}"]`
|
|
207
|
-
: "")) as Uppercase<string>,
|
|
208
|
-
};
|
|
179
|
+
return { description, hint, label: labelMultiple };
|
|
209
180
|
},
|
|
210
181
|
consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
|
|
211
182
|
const positionals = new Array<string>();
|
|
@@ -222,7 +193,7 @@ export function positionalVariadics<Value>(definition: {
|
|
|
222
193
|
return {
|
|
223
194
|
decodeValue() {
|
|
224
195
|
return positionals.map((positional) =>
|
|
225
|
-
decodeValue(
|
|
196
|
+
decodeValue(labelSingle, definition.type, positional),
|
|
226
197
|
);
|
|
227
198
|
},
|
|
228
199
|
};
|
|
@@ -237,11 +208,15 @@ function decodeValue<Value>(
|
|
|
237
208
|
): Value {
|
|
238
209
|
return TypoError.tryWithContext(
|
|
239
210
|
() => type.decoder(input),
|
|
240
|
-
() =>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
),
|
|
246
221
|
);
|
|
247
222
|
}
|
package/src/lib/Reader.ts
CHANGED
|
@@ -17,11 +17,11 @@ export type ReaderOptionKey = (string | { __brand: "ReaderOptionKey" }) & {
|
|
|
17
17
|
* Parsing behaviour for a registered option, passed to {@link ReaderArgs.registerOption}.
|
|
18
18
|
*/
|
|
19
19
|
export type ReaderOptionParsing = {
|
|
20
|
-
consumeShortGroup: boolean;
|
|
20
|
+
consumeShortGroup: boolean; // TODO - this doesnt matter when no short option
|
|
21
21
|
consumeNextArg: (
|
|
22
22
|
inlined: string | null,
|
|
23
23
|
separated: Array<string>,
|
|
24
|
-
|
|
24
|
+
nextArg: string | undefined,
|
|
25
25
|
) => boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -340,7 +340,7 @@ export class ReaderArgs {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
#isValidOptionName(name: string): boolean {
|
|
343
|
-
return name.length > 0 && !name.includes("=");
|
|
343
|
+
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|