cli-kiss 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,20 +8,19 @@ import {
8
8
  typoStyleUserInput,
9
9
  TypoText,
10
10
  } from "./Typo";
11
+ import { UsageOption } from "./Usage";
11
12
 
12
13
  /**
13
- * A CLI option (flag or valued) with its parsing and usage-generation logic.
14
- *
15
- * Created with {@link optionFlag}, {@link optionSingleValue}, or
16
- * {@link optionRepeatable} and passed via the `options` map of {@link operation}.
14
+ * A CLI option. Created with {@link optionFlag}, {@link optionSingleValue},
15
+ * or {@link optionRepeatable}.
17
16
  *
18
17
  * @typeParam Value - Decoded value type.
19
18
  */
20
19
  export type Option<Value> = {
21
20
  /**
22
- * Returns metadata used to render the `Options:` section of help.
21
+ * Returns metadata for the `Options:` section.
23
22
  */
24
- generateUsage(): OptionUsage;
23
+ generateUsage(): UsageOption;
25
24
  /**
26
25
  * Registers the option on `readerOptions` and returns an {@link OptionDecoder}.
27
26
  */
@@ -42,46 +41,18 @@ export type OptionDecoder<Value> = {
42
41
  getAndDecodeValue(): Value;
43
42
  };
44
43
 
45
- /**
46
- * Human-readable metadata for a single option, used to render the `Options:` section
47
- * of the help output produced by {@link usageToStyledLines}.
48
- */
49
- export type OptionUsage = {
50
- /**
51
- * Long-form name without `--` (e.g. `"verbose"`).
52
- */
53
- long: Lowercase<string>;
54
- /**
55
- * Short-form name without `-` (e.g. `"v"`).
56
- */
57
- short: string | undefined;
58
- /**
59
- * Help text in usage.
60
- */
61
- description: string | undefined;
62
- /**
63
- * Short note shown in parentheses.
64
- */
65
- hint: string | undefined;
66
- /**
67
- * Value placeholder in help (e.g. `"<FILE>"`). `undefined` for flags.
68
- */
69
- label: Uppercase<string> | undefined;
70
- };
71
-
72
44
  /**
73
45
  * Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
74
46
  *
75
- * Parsing: absent → `false`; `--flag` / `--flag=yes` → `true`; `--flag=no` → `false`;
76
- * 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}.
77
49
  *
78
- * @param definition - Flag configuration.
79
50
  * @param definition.long - Long-form name (without `--`).
80
51
  * @param definition.short - Short-form name (without `-`).
81
52
  * @param definition.description - Help text.
82
53
  * @param definition.hint - Short note shown in parentheses.
83
54
  * @param definition.aliases - Additional names.
84
- * @param definition.default - Default when absent. Defaults to `() => false`.
55
+ * @param definition.default - Default value when absent.
85
56
  * @returns An {@link Option}`<boolean>`.
86
57
  *
87
58
  * @example
@@ -91,64 +62,50 @@ export type OptionUsage = {
91
62
  * short: "v",
92
63
  * description: "Enable verbose output",
93
64
  * });
65
+ * // Usage:
66
+ * // my-cli → false
67
+ * // my-cli --verbose → true
68
+ * // my-cli --verbose=yes → true
69
+ * // my-cli -v=no → false
94
70
  * ```
95
71
  */
96
72
  export function optionFlag(definition: {
97
- long: Lowercase<string>;
73
+ long: string;
98
74
  short?: string;
99
75
  description?: string;
100
76
  hint?: string;
101
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
102
- default?: () => boolean;
77
+ aliases?: { longs?: Array<string>; shorts?: Array<string> };
78
+ default?: boolean;
103
79
  }): Option<boolean> {
104
- const label = `<${typeBoolean.content.toUpperCase()}>`;
80
+ const type = typeBoolean("value");
81
+ const { long, short, description, hint, aliases } = definition;
105
82
  return {
106
83
  generateUsage() {
107
- return {
108
- description: definition.description,
109
- hint: definition.hint,
110
- long: definition.long,
111
- short: definition.short,
112
- label: undefined,
113
- };
84
+ return { short, long, annotation: "[=no]", description, hint };
114
85
  },
115
86
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
116
87
  const key = registerOption(readerOptions, {
117
- ...definition,
118
- valued: false,
88
+ long,
89
+ short,
90
+ aliasesLongs: aliases?.longs,
91
+ aliasesShorts: aliases?.shorts,
92
+ parsing: { consumeShortGroup: false, consumeNextArg: () => false },
119
93
  });
120
94
  return {
121
95
  getAndDecodeValue() {
122
- const optionValues = readerOptions.getOptionValues(key);
123
- if (optionValues.length > 1) {
124
- throw new TypoError(
125
- new TypoText(
126
- new TypoString(`--${definition.long}`, typoStyleConstants),
127
- new TypoString(`: Must not be set multiple times`),
128
- ),
129
- );
96
+ const optionResults = readerOptions.getOptionValues(key);
97
+ if (optionResults.length > 1) {
98
+ throwSetMultipleTimesError(long);
130
99
  }
131
- const optionValue = optionValues[0];
132
- if (optionValue === undefined) {
133
- try {
134
- return definition.default ? definition.default() : false;
135
- } catch (error) {
136
- throw new TypoError(
137
- new TypoText(
138
- new TypoString(`--${definition.long}`, typoStyleConstants),
139
- new TypoString(`: Failed to get default value`),
140
- ),
141
- error,
142
- );
143
- }
100
+ if (optionResults.length === 0) {
101
+ return definition.default === undefined
102
+ ? false
103
+ : definition.default;
144
104
  }
145
- return decodeValue({
146
- long: definition.long,
147
- short: definition.short,
148
- label,
149
- type: typeBoolean,
150
- input: optionValue,
151
- });
105
+ const positiveResult = optionResults[0]!;
106
+ const value =
107
+ positiveResult.inlined === null ? "true" : positiveResult.inlined;
108
+ return decodeValue({ long, short, type, input: value });
152
109
  },
153
110
  };
154
111
  },
@@ -158,20 +115,19 @@ export function optionFlag(definition: {
158
115
  /**
159
116
  * Creates an option that accepts exactly one value (e.g. `--output dist/`).
160
117
  *
161
- * Parsing: absent → `default()`; once → decoded with `type`; more than once → {@link TypoError}.
118
+ * Parsing: absent → `defaultValue()`; once → decoded with `type`; more than once → {@link TypoError}.
162
119
  * Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
163
120
  *
164
121
  * @typeParam Value - Type produced by the decoder.
165
122
  *
166
- * @param definition - Option configuration.
167
123
  * @param definition.long - Long-form name (without `--`).
168
124
  * @param definition.short - Short-form name (without `-`).
169
125
  * @param definition.description - Help text.
170
126
  * @param definition.hint - Short note shown in parentheses.
171
127
  * @param definition.aliases - Additional names.
172
- * @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
173
128
  * @param definition.type - Decoder for the raw string value.
174
- * @param definition.default - Default when absent. Throw to make the option required.
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`).
175
131
  * @returns An {@link Option}`<Value>`.
176
132
  *
177
133
  * @example
@@ -179,71 +135,75 @@ export function optionFlag(definition: {
179
135
  * const outputOption = optionSingleValue({
180
136
  * long: "output",
181
137
  * short: "o",
182
- * type: typeString,
183
- * label: "PATH",
138
+ * type: typePath(),
184
139
  * description: "Output directory",
185
- * default: () => "dist/",
140
+ * valueWhenNotDefined: () => "dist",
186
141
  * });
142
+ * // Usage:
143
+ * // my-cli → "dist"
144
+ * // my-cli --output folder → "folder"
145
+ * // my-cli -o folder → "folder"
187
146
  * ```
188
147
  */
189
148
  export function optionSingleValue<Value>(definition: {
190
- long: Lowercase<string>;
149
+ long: string;
191
150
  short?: string;
192
151
  description?: string;
193
152
  hint?: string;
194
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
195
- label?: Uppercase<string>;
153
+ aliases?: { longs?: Array<string>; shorts?: Array<string> };
196
154
  type: Type<Value>;
197
- default: () => Value;
155
+ defaultWhenNotDefined: () => Value;
156
+ defaultWhenNotInlined?: () => Value;
198
157
  }): Option<Value> {
199
- const label = `<${definition.label ?? definition.type.content.toUpperCase()}>`;
158
+ const { long, short, description, hint, aliases, type } = definition;
159
+ const label = `<${type.content}>`;
200
160
  return {
201
161
  generateUsage() {
202
- return {
203
- description: definition.description,
204
- hint: definition.hint,
205
- long: definition.long,
206
- short: definition.short,
207
- label: label as Uppercase<string>,
208
- };
162
+ return { short, long, label, description, hint };
209
163
  },
210
164
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
211
165
  const key = registerOption(readerOptions, {
212
- ...definition,
213
- valued: true,
166
+ long,
167
+ short,
168
+ aliasesLongs: aliases?.longs,
169
+ aliasesShorts: aliases?.shorts,
170
+ parsing: {
171
+ consumeShortGroup: true,
172
+ consumeNextArg(inlined, separated) {
173
+ if (definition.defaultWhenNotInlined !== undefined) {
174
+ return false;
175
+ }
176
+ return inlined === null && separated.length === 0;
177
+ },
178
+ },
214
179
  });
215
180
  return {
216
181
  getAndDecodeValue() {
217
- const optionValues = readerOptions.getOptionValues(key);
218
- if (optionValues.length > 1) {
219
- throw new TypoError(
220
- new TypoText(
221
- new TypoString(`--${definition.long}`, typoStyleConstants),
222
- new TypoString(`: Requires a single value, but got multiple`),
223
- ),
224
- );
182
+ const optionResults = readerOptions.getOptionValues(key);
183
+ if (optionResults.length > 1) {
184
+ throwSetMultipleTimesError(long);
225
185
  }
226
- const optionValue = optionValues[0];
227
- if (optionValue === undefined) {
186
+ const optionResult = optionResults[0];
187
+ if (optionResult === undefined) {
228
188
  try {
229
- return definition.default();
189
+ return definition.defaultWhenNotDefined();
230
190
  } catch (error) {
231
- throw new TypoError(
232
- new TypoText(
233
- new TypoString(`--${definition.long}`, typoStyleConstants),
234
- new TypoString(`: Failed to get default value`),
235
- ),
236
- error,
237
- );
191
+ throwFailedToGetDefaultValueError(long, error, "not set");
238
192
  }
239
193
  }
240
- return decodeValue({
241
- long: definition.long,
242
- short: definition.short,
243
- label,
244
- type: definition.type,
245
- input: optionValue,
246
- });
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 });
247
207
  },
248
208
  };
249
209
  },
@@ -258,13 +218,11 @@ export function optionSingleValue<Value>(definition: {
258
218
  *
259
219
  * @typeParam Value - Type produced by the decoder for each occurrence.
260
220
  *
261
- * @param definition - Option configuration.
262
221
  * @param definition.long - Long-form name (without `--`).
263
222
  * @param definition.short - Short-form name (without `-`).
264
223
  * @param definition.description - Help text.
265
224
  * @param definition.hint - Short note shown in parentheses.
266
225
  * @param definition.aliases - Additional names.
267
- * @param definition.label - Value placeholder in help. Defaults to uppercased `type.content`.
268
226
  * @param definition.type - Decoder applied to each raw string value.
269
227
  * @returns An {@link Option}`<Array<Value>>`.
270
228
  *
@@ -281,43 +239,38 @@ export function optionSingleValue<Value>(definition: {
281
239
  * ```
282
240
  */
283
241
  export function optionRepeatable<Value>(definition: {
284
- long: Lowercase<string>;
242
+ long: string;
285
243
  short?: string;
286
244
  description?: string;
287
245
  hint?: string;
288
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
289
- label?: Uppercase<string>;
246
+ aliases?: { longs?: Array<string>; shorts?: Array<string> };
290
247
  type: Type<Value>;
291
248
  }): Option<Array<Value>> {
292
- const label = `<${definition.label ?? definition.type.content.toUpperCase()}>`;
249
+ const { long, short, description, hint, aliases, type } = definition;
250
+ const label = `<${type.content}>`;
293
251
  return {
294
252
  generateUsage() {
295
- // TODO - showcase that it can be repeated ?
296
- return {
297
- description: definition.description,
298
- hint: definition.hint,
299
- long: definition.long,
300
- short: definition.short,
301
- label: label as Uppercase<string>,
302
- };
253
+ return { short, long, label, annotation: " [*]", description, hint };
303
254
  },
304
255
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
305
256
  const key = registerOption(readerOptions, {
306
- ...definition,
307
- valued: true,
257
+ long,
258
+ short,
259
+ aliasesLongs: aliases?.longs,
260
+ aliasesShorts: aliases?.shorts,
261
+ parsing: {
262
+ consumeShortGroup: true,
263
+ consumeNextArg: (inlined, separated) =>
264
+ inlined === null && separated.length === 0,
265
+ },
308
266
  });
309
267
  return {
310
268
  getAndDecodeValue() {
311
- const optionValues = readerOptions.getOptionValues(key);
312
- return optionValues.map((optionValue) =>
313
- decodeValue({
314
- long: definition.long,
315
- short: definition.short,
316
- label,
317
- type: definition.type,
318
- input: optionValue,
319
- }),
320
- );
269
+ const optionResults = readerOptions.getOptionValues(key);
270
+ return optionResults.map((optionResult) => {
271
+ const input = optionResult.inlined ?? optionResult.separated[0]!;
272
+ return decodeValue({ long, short, label, type, input });
273
+ });
321
274
  },
322
275
  };
323
276
  },
@@ -326,8 +279,8 @@ export function optionRepeatable<Value>(definition: {
326
279
 
327
280
  function decodeValue<Value>(params: {
328
281
  long: string;
329
- short: string | undefined;
330
- label: string;
282
+ short?: string | undefined;
283
+ label?: string | undefined;
331
284
  type: Type<Value>;
332
285
  input: string;
333
286
  }): Value {
@@ -336,14 +289,17 @@ function decodeValue<Value>(params: {
336
289
  () => {
337
290
  const text = new TypoText();
338
291
  if (params.short) {
339
- text.pushString(new TypoString(`-${params.short}`, typoStyleConstants));
340
- text.pushString(new TypoString(`, `));
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));
341
302
  }
342
- text.pushString(new TypoString(`--${params.long}`, typoStyleConstants));
343
- text.pushString(new TypoString(`: `));
344
- text.pushString(new TypoString(params.label, typoStyleUserInput));
345
- text.pushString(new TypoString(`: `));
346
- text.pushString(new TypoString(params.type.content, typoStyleLogic));
347
303
  return text;
348
304
  },
349
305
  );
@@ -352,20 +308,44 @@ function decodeValue<Value>(params: {
352
308
  function registerOption(
353
309
  readerOptions: ReaderOptions,
354
310
  definition: {
355
- long: Lowercase<string>;
356
- short?: string;
357
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
358
- valued: boolean;
311
+ long: string;
312
+ short: undefined | string;
313
+ aliasesLongs: undefined | Array<string>;
314
+ aliasesShorts: undefined | Array<string>;
315
+ parsing: ReaderOptionParsing;
359
316
  },
360
317
  ) {
361
- const { long, short, aliases, valued } = definition;
318
+ const { long, short, aliasesLongs, aliasesShorts, parsing } = definition;
362
319
  const longs = long ? [long] : [];
363
- if (aliases?.longs) {
364
- longs.push(...aliases?.longs);
320
+ if (aliasesLongs) {
321
+ longs.push(...aliasesLongs);
365
322
  }
366
323
  const shorts = short ? [short] : [];
367
- if (aliases?.shorts) {
368
- shorts.push(...aliases?.shorts);
324
+ if (aliasesShorts) {
325
+ shorts.push(...aliasesShorts);
369
326
  }
370
- return readerOptions.registerOption({ longs, shorts, valued });
327
+ return readerOptions.registerOption({ longs, shorts, parsing });
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
+ );
371
351
  }