cli-kiss 0.2.2 → 0.2.4

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/src/lib/Option.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReaderArgs as ReaderOptions } from "./Reader";
1
+ import { ReaderOptionParsing, ReaderArgs as ReaderOptions } from "./Reader";
2
2
  import { Type, typeBoolean } from "./Type";
3
3
  import {
4
4
  TypoError,
@@ -8,91 +8,51 @@ import {
8
8
  typoStyleUserInput,
9
9
  TypoText,
10
10
  } from "./Typo";
11
+ import { UsageOption } from "./Usage";
11
12
 
12
13
  /**
13
- * Describes a single CLI option (a flag or a valued option) together with its parsing
14
- * and usage-generation logic.
14
+ * A CLI option. Created with {@link optionFlag}, {@link optionSingleValue},
15
+ * or {@link optionRepeatable}.
15
16
  *
16
- * Options are created with {@link optionFlag}, {@link optionSingleValue}, or
17
- * {@link optionRepeatable} and are passed via the `options` map of {@link operation}.
18
- *
19
- * @typeParam Value - The TypeScript type of the parsed option value.
20
- * - `boolean` for flags created with {@link optionFlag}.
21
- * - `T` for single-value options created with {@link optionSingleValue}.
22
- * - `Array<T>` for repeatable options created with {@link optionRepeatable}.
17
+ * @typeParam Value - Decoded value type.
23
18
  */
24
19
  export type Option<Value> = {
25
- /** Returns human-readable metadata used to render the `Options:` section of help. */
26
- generateUsage(): OptionUsage;
27
20
  /**
28
- * Registers the option on `readerOptions` so the argument reader recognises it, and
29
- * returns an {@link OptionParser} that can later retrieve the parsed value(s).
30
- *
31
- * @param readerOptions - The shared {@link ReaderArgs} that will parse the raw
32
- * command-line tokens.
21
+ * Returns metadata for the `Options:` section.
33
22
  */
34
- createParser(readerOptions: ReaderOptions): OptionParser<Value>;
23
+ generateUsage(): UsageOption;
24
+ /**
25
+ * Registers the option on `readerOptions` and returns an {@link OptionDecoder}.
26
+ */
27
+ registerAndMakeDecoder(readerOptions: ReaderOptions): OptionDecoder<Value>;
35
28
  };
36
29
 
37
30
  /**
38
- * Retrieves the parsed value for a registered option after argument parsing is complete.
39
- *
40
- * Returned by {@link Option.createParser} and called by {@link OperationFactory.createInstance}.
31
+ * Produced by {@link Option.registerAndMakeDecoder}.
41
32
  *
42
- * @typeParam Value - The TypeScript type of the parsed value.
43
- */
44
- export type OptionParser<Value> = {
45
- parseValue(): Value;
46
- };
47
-
48
- /**
49
- * Human-readable metadata for a single CLI option, used to render the `Options:` section
50
- * of the help output produced by {@link usageToStyledLines}.
33
+ * @typeParam Value - Decoded value type.
51
34
  */
52
- export type OptionUsage = {
53
- /** Short description of what the option does. */
54
- description: string | undefined;
35
+ export type OptionDecoder<Value> = {
55
36
  /**
56
- * Optional supplementary note shown in parentheses next to the description.
57
- * Suitable for short caveats such as `"required"` or `"defaults to 42"`.
58
- */
59
- hint: string | undefined;
60
- /**
61
- * The primary long-form name of the option, without the `--` prefix (e.g. `"verbose"`).
62
- * The user passes this as `--verbose` on the command line.
63
- */
64
- long: Lowercase<string>; // TODO - better type for long option names ?
65
- /**
66
- * The optional short-form name of the option, without the `-` prefix (e.g. `"v"`).
67
- * The user passes this as `-v` on the command line.
68
- */
69
- short: string | undefined;
70
- /**
71
- * The value placeholder label shown after the long option name in the help output
72
- * (e.g. `"<FILE>"`). `undefined` for flags that take no value.
37
+ * Returns the decoded option value.
38
+ *
39
+ * @throws {@link TypoError} if decoding failed.
73
40
  */
74
- label: Uppercase<string> | undefined;
41
+ getAndDecodeValue(): Value;
75
42
  };
76
43
 
77
44
  /**
78
- * Creates a boolean flag option an option that the user passes without a value (e.g.
79
- * `--verbose`) to signal `true`, or can explicitly set with `--flag=true` / `--flag=no`.
45
+ * Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
80
46
  *
81
- * **Parsing rules:**
82
- * - Absent `false` (or the return value of `default()` when provided).
83
- * - `--flag` / `--flag=true` / `--flag=yes` → `true`.
84
- * - `--flag=false` / `--flag=no` → `false`.
85
- * - Specified more than once → {@link TypoError} ("Must not be set multiple times").
47
+ * Parsing: absent → `false`; `--flag` / `--flag=yes` → `true`; `--flag=no` → `false`;
48
+ * specified more than once {@link TypoError}.
86
49
  *
87
- * @param definition - Configuration for the flag.
88
- * @param definition.long - Primary long-form name (without `--`). Must be lowercase.
89
- * @param definition.short - Optional short-form name (without `-`).
90
- * @param definition.description - Human-readable description for the help output.
91
- * @param definition.hint - Optional supplementary note shown in parentheses.
92
- * @param definition.aliases - Additional long/short names that the parser also
93
- * recognises as this flag.
94
- * @param definition.default - Factory for the default value when the flag is absent.
95
- * Defaults to `() => false` when omitted.
50
+ * @param definition.long - Long-form name (without `--`).
51
+ * @param definition.short - Short-form name (without `-`).
52
+ * @param definition.description - Help text.
53
+ * @param definition.hint - Short note shown in parentheses.
54
+ * @param definition.aliases - Additional names.
55
+ * @param definition.default - Default when absent. Defaults to `false`.
96
56
  * @returns An {@link Option}`<boolean>`.
97
57
  *
98
58
  * @example
@@ -110,28 +70,44 @@ export function optionFlag(definition: {
110
70
  description?: string;
111
71
  hint?: string;
112
72
  aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
113
- default?: () => boolean;
73
+ default?: boolean;
114
74
  }): Option<boolean> {
115
75
  const label = `<${typeBoolean.content.toUpperCase()}>`;
116
76
  return {
117
77
  generateUsage() {
118
78
  return {
119
- description: definition.description,
120
- hint: definition.hint,
121
- long: definition.long,
122
79
  short: definition.short,
80
+ long: definition.long,
123
81
  label: undefined,
82
+ annotation: "[=no]",
83
+ description: definition.description,
84
+ hint: definition.hint,
124
85
  };
125
86
  },
126
- createParser(readerOptions: ReaderOptions) {
127
- const key = registerOption(readerOptions, {
128
- ...definition,
129
- valued: false,
87
+ registerAndMakeDecoder(readerOptions: ReaderOptions) {
88
+ const longNegative = `no-${definition.long}` as Lowercase<string>;
89
+ const aliasesLongsNegatives = definition.aliases?.longs?.map(
90
+ (aliasLong) => `no-${aliasLong}` as Lowercase<string>,
91
+ );
92
+ const keyNegative = registerOption(readerOptions, {
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,
104
+ parsing: { consumeShortGroup: false, consumeNextArg: () => false },
130
105
  });
131
106
  return {
132
- parseValue() {
133
- const optionValues = readerOptions.getOptionValues(key);
134
- if (optionValues.length > 1) {
107
+ getAndDecodeValue() {
108
+ const negativeResults = readerOptions.getOptionValues(keyNegative);
109
+ const positiveResults = readerOptions.getOptionValues(keyPositive);
110
+ if (positiveResults.length > 1) {
135
111
  throw new TypoError(
136
112
  new TypoText(
137
113
  new TypoString(`--${definition.long}`, typoStyleConstants),
@@ -139,21 +115,49 @@ export function optionFlag(definition: {
139
115
  ),
140
116
  );
141
117
  }
142
- const optionValue = optionValues[0];
143
- if (optionValue === undefined) {
144
- try {
145
- return definition.default ? definition.default() : false;
146
- } catch (error) {
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) {
147
138
  throw new TypoError(
148
139
  new TypoText(
149
- new TypoString(`--${definition.long}`, typoStyleConstants),
150
- new TypoString(`: Failed to get default value`),
140
+ new TypoString(`--${longNegative}`, typoStyleConstants),
141
+ new TypoString(`: Must not have a value`),
151
142
  ),
152
- error,
153
143
  );
154
144
  }
145
+ return false;
155
146
  }
156
- return decodeValue(definition.long, label, typeBoolean, optionValue);
147
+ if (positiveResults.length > 0) {
148
+ const positiveResult = positiveResults[0]!;
149
+ return decodeValue({
150
+ long: definition.long,
151
+ short: definition.short,
152
+ label,
153
+ type: typeBoolean,
154
+ input:
155
+ positiveResult.inlined === null
156
+ ? "true"
157
+ : positiveResult.inlined,
158
+ });
159
+ }
160
+ return definition.default ?? false;
157
161
  },
158
162
  };
159
163
  },
@@ -161,32 +165,21 @@ export function optionFlag(definition: {
161
165
  }
162
166
 
163
167
  /**
164
- * Creates an option that accepts exactly one value (e.g. `--output dist/` or
165
- * `--output=dist/`).
166
- *
167
- * **Parsing rules:**
168
- * - Absent → `definition.default()` is called. If the default factory throws, a
169
- * {@link TypoError} is produced.
170
- * - Specified once → the value is decoded with `definition.type`.
171
- * - Specified more than once → {@link TypoError} ("Requires a single value, but got
172
- * multiple").
168
+ * Creates an option that accepts exactly one value (e.g. `--output dist/`).
173
169
  *
174
- * **Value syntax:** `--long value`, `--long=value`, or (if `short` is set) `-s value`,
175
- * `-s=value`, or `-svalue`.
170
+ * Parsing: absent `default()`; once decoded with `type`; more than once → {@link TypoError}.
171
+ * Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
176
172
  *
177
- * @typeParam Value - The TypeScript type produced by the type decoder.
173
+ * @typeParam Value - Type produced by the decoder.
178
174
  *
179
- * @param definition - Configuration for the option.
180
- * @param definition.long - Primary long-form name (without `--`). Must be lowercase.
181
- * @param definition.short - Optional short-form name (without `-`).
182
- * @param definition.description - Human-readable description for the help output.
183
- * @param definition.hint - Optional supplementary note shown in parentheses.
184
- * @param definition.aliases - Additional long/short names the parser also recognises.
185
- * @param definition.label - Custom label shown in the help output (e.g. `"FILE"`).
186
- * Defaults to the uppercased `type.content`.
187
- * @param definition.type - The {@link Type} used to decode the raw string value.
188
- * @param definition.default - Factory for the default value when the option is absent.
189
- * Throw an error from this factory to make the option effectively required.
175
+ * @param definition.long - Long-form name (without `--`).
176
+ * @param definition.short - Short-form name (without `-`).
177
+ * @param definition.description - Help text.
178
+ * @param definition.hint - Short note shown in parentheses.
179
+ * @param definition.aliases - Additional names.
180
+ * @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
181
+ * @param definition.type - Decoder for the raw string value.
182
+ * @param definition.default - Default when absent. Throw to make the option required.
190
183
  * @returns An {@link Option}`<Value>`.
191
184
  *
192
185
  * @example
@@ -215,22 +208,30 @@ export function optionSingleValue<Value>(definition: {
215
208
  return {
216
209
  generateUsage() {
217
210
  return {
218
- description: definition.description,
219
- hint: definition.hint,
220
- long: definition.long,
221
211
  short: definition.short,
212
+ long: definition.long,
222
213
  label: label as Uppercase<string>,
214
+ annotation: undefined,
215
+ description: definition.description,
216
+ hint: definition.hint,
223
217
  };
224
218
  },
225
- createParser(readerOptions: ReaderOptions) {
219
+ registerAndMakeDecoder(readerOptions: ReaderOptions) {
226
220
  const key = registerOption(readerOptions, {
227
- ...definition,
228
- valued: true,
221
+ long: definition.long,
222
+ short: definition.short,
223
+ aliasesLongs: definition.aliases?.longs,
224
+ aliasesShorts: definition.aliases?.shorts,
225
+ parsing: {
226
+ consumeShortGroup: true,
227
+ consumeNextArg: (inlined, separated) =>
228
+ inlined === null && separated.length === 0,
229
+ },
229
230
  });
230
231
  return {
231
- parseValue() {
232
- const optionValues = readerOptions.getOptionValues(key);
233
- if (optionValues.length > 1) {
232
+ getAndDecodeValue() {
233
+ const optionResults = readerOptions.getOptionValues(key);
234
+ if (optionResults.length > 1) {
234
235
  throw new TypoError(
235
236
  new TypoText(
236
237
  new TypoString(`--${definition.long}`, typoStyleConstants),
@@ -238,8 +239,8 @@ export function optionSingleValue<Value>(definition: {
238
239
  ),
239
240
  );
240
241
  }
241
- const optionValue = optionValues[0];
242
- if (optionValue === undefined) {
242
+ const optionResult = optionResults[0];
243
+ if (optionResult === undefined) {
243
244
  try {
244
245
  return definition.default();
245
246
  } catch (error) {
@@ -252,12 +253,13 @@ export function optionSingleValue<Value>(definition: {
252
253
  );
253
254
  }
254
255
  }
255
- return decodeValue(
256
- definition.long,
256
+ return decodeValue({
257
+ long: definition.long,
258
+ short: definition.short,
257
259
  label,
258
- definition.type,
259
- optionValue,
260
- );
260
+ type: definition.type,
261
+ input: optionResult.inlined ?? optionResult.separated[0]!,
262
+ });
261
263
  },
262
264
  };
263
265
  },
@@ -265,30 +267,20 @@ export function optionSingleValue<Value>(definition: {
265
267
  }
266
268
 
267
269
  /**
268
- * Creates an option that can be specified any number of times, collecting all provided
269
- * values into an array (e.g. `--file a.ts --file b.ts`).
270
- *
271
- * **Parsing rules:**
272
- * - Absent → empty array `[]`.
273
- * - Specified N times → array of N decoded values, in the order they appear on the
274
- * command line.
275
- * - Each occurrence is decoded independently with `definition.type`.
270
+ * Creates an option that collects every occurrence into an array (e.g. `--file a.ts --file b.ts`).
276
271
  *
277
- * **Value syntax:** `--long value`, `--long=value`, or (if `short` is set) `-s value`,
278
- * `-s=value`, or `-svalue`.
272
+ * Parsing: absent `[]`; N occurrences array of N decoded values in order.
273
+ * Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
279
274
  *
280
- * @typeParam Value - The TypeScript type produced by the type decoder for each
281
- * occurrence.
275
+ * @typeParam Value - Type produced by the decoder for each occurrence.
282
276
  *
283
- * @param definition - Configuration for the option.
284
- * @param definition.long - Primary long-form name (without `--`). Must be lowercase.
285
- * @param definition.short - Optional short-form name (without `-`).
286
- * @param definition.description - Human-readable description for the help output.
287
- * @param definition.hint - Optional supplementary note shown in parentheses.
288
- * @param definition.aliases - Additional long/short names the parser also recognises.
289
- * @param definition.label - Custom label shown in the help output (e.g. `"FILE"`).
290
- * Defaults to the uppercased `type.content`.
291
- * @param definition.type - The {@link Type} used to decode each raw string value.
277
+ * @param definition.long - Long-form name (without `--`).
278
+ * @param definition.short - Short-form name (without `-`).
279
+ * @param definition.description - Help text.
280
+ * @param definition.hint - Short note shown in parentheses.
281
+ * @param definition.aliases - Additional names.
282
+ * @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
283
+ * @param definition.type - Decoder applied to each raw string value.
292
284
  * @returns An {@link Option}`<Array<Value>>`.
293
285
  *
294
286
  * @example
@@ -317,23 +309,37 @@ export function optionRepeatable<Value>(definition: {
317
309
  generateUsage() {
318
310
  // TODO - showcase that it can be repeated ?
319
311
  return {
320
- description: definition.description,
321
- hint: definition.hint,
322
- long: definition.long,
323
312
  short: definition.short,
313
+ long: definition.long,
324
314
  label: label as Uppercase<string>,
315
+ annotation: " [*]",
316
+ description: definition.description,
317
+ hint: definition.hint,
325
318
  };
326
319
  },
327
- createParser(readerOptions: ReaderOptions) {
320
+ registerAndMakeDecoder(readerOptions: ReaderOptions) {
328
321
  const key = registerOption(readerOptions, {
329
- ...definition,
330
- valued: true,
322
+ long: definition.long,
323
+ short: definition.short,
324
+ aliasesLongs: definition.aliases?.longs,
325
+ aliasesShorts: definition.aliases?.shorts,
326
+ parsing: {
327
+ consumeShortGroup: true,
328
+ consumeNextArg: (inlined, separated) =>
329
+ inlined === null && separated.length === 0,
330
+ },
331
331
  });
332
332
  return {
333
- parseValue() {
334
- const optionValues = readerOptions.getOptionValues(key);
335
- return optionValues.map((optionValue) =>
336
- decodeValue(definition.long, label, definition.type, optionValue),
333
+ getAndDecodeValue() {
334
+ const optionResults = readerOptions.getOptionValues(key);
335
+ return optionResults.map((optionResult) =>
336
+ decodeValue({
337
+ long: definition.long,
338
+ short: definition.short,
339
+ label,
340
+ type: definition.type,
341
+ input: optionResult.inlined ?? optionResult.separated[0]!,
342
+ }),
337
343
  );
338
344
  },
339
345
  };
@@ -341,22 +347,28 @@ export function optionRepeatable<Value>(definition: {
341
347
  };
342
348
  }
343
349
 
344
- function decodeValue<Value>(
345
- long: string,
346
- label: string,
347
- type: Type<Value>,
348
- value: string,
349
- ): Value {
350
+ function decodeValue<Value>(params: {
351
+ long: string;
352
+ short: string | undefined;
353
+ label: string;
354
+ type: Type<Value>;
355
+ input: string;
356
+ }): Value {
350
357
  return TypoError.tryWithContext(
351
- () => type.decoder(value),
352
- () =>
353
- new TypoText(
354
- new TypoString(`--${long}`, typoStyleConstants),
355
- new TypoString(`: `),
356
- new TypoString(label, typoStyleUserInput),
357
- new TypoString(`: `),
358
- new TypoString(type.content, typoStyleLogic),
359
- ),
358
+ () => params.type.decoder(params.input),
359
+ () => {
360
+ const text = new TypoText();
361
+ if (params.short) {
362
+ text.push(new TypoString(`-${params.short}`, typoStyleConstants));
363
+ text.push(new TypoString(`, `));
364
+ }
365
+ text.push(new TypoString(`--${params.long}`, typoStyleConstants));
366
+ text.push(new TypoString(`: `));
367
+ text.push(new TypoString(params.label, typoStyleUserInput));
368
+ text.push(new TypoString(`: `));
369
+ text.push(new TypoString(params.type.content, typoStyleLogic));
370
+ return text;
371
+ },
360
372
  );
361
373
  }
362
374
 
@@ -364,19 +376,20 @@ function registerOption(
364
376
  readerOptions: ReaderOptions,
365
377
  definition: {
366
378
  long: Lowercase<string>;
367
- short?: string;
368
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
369
- valued: boolean;
379
+ short: undefined | string;
380
+ aliasesLongs: undefined | Array<Lowercase<string>>;
381
+ aliasesShorts: undefined | Array<string>;
382
+ parsing: ReaderOptionParsing;
370
383
  },
371
384
  ) {
372
- const { long, short, aliases, valued } = definition;
385
+ const { long, short, aliasesLongs, aliasesShorts, parsing } = definition;
373
386
  const longs = long ? [long] : [];
374
- if (aliases?.longs) {
375
- longs.push(...aliases?.longs);
387
+ if (aliasesLongs) {
388
+ longs.push(...aliasesLongs);
376
389
  }
377
390
  const shorts = short ? [short] : [];
378
- if (aliases?.shorts) {
379
- shorts.push(...aliases?.shorts);
391
+ if (aliasesShorts) {
392
+ shorts.push(...aliasesShorts);
380
393
  }
381
- return readerOptions.registerOption({ longs, shorts, valued });
394
+ return readerOptions.registerOption({ longs, shorts, parsing });
382
395
  }