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.
@@ -1,27 +1,19 @@
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";
4
+ import { UsagePositional } from "./Usage";
10
5
 
11
6
  /**
12
- * A bare (non-option) positional argument with its parsing and usage-generation logic.
13
- *
14
- * Created with {@link positionalRequired}, {@link positionalOptional}, or
15
- * {@link positionalVariadics} and passed via the `positionals` array of
16
- * {@link operation}, consumed in declaration order.
7
+ * A positional argument. Created with {@link positionalRequired}, {@link positionalOptional},
8
+ * or {@link positionalVariadics}.
17
9
  *
18
10
  * @typeParam Value - Decoded value type.
19
11
  */
20
12
  export type Positional<Value> = {
21
13
  /**
22
- * Returns metadata used to render the `Positionals:` section of help.
14
+ * Returns metadata for the `Positionals:` section.
23
15
  */
24
- generateUsage(): PositionalUsage;
16
+ generateUsage(): UsagePositional;
25
17
  /**
26
18
  * Consumes the next positional token from `readerPositionals`.
27
19
  * Returns a decoder that produces the final value.
@@ -45,63 +37,36 @@ export type PositionalDecoder<Value> = {
45
37
  decodeValue(): Value;
46
38
  };
47
39
 
48
- /**
49
- * Human-readable metadata for a single positional argument, used to render the
50
- * `Positionals:` section of the help output produced by {@link usageToStyledLines}.
51
- */
52
- export type PositionalUsage = {
53
- /**
54
- * Help text.
55
- */
56
- description: string | undefined;
57
- /**
58
- * Short note shown in parentheses.
59
- */
60
- hint: string | undefined;
61
- /**
62
- * Placeholder label shown in the usage line and the `Positionals:` section.
63
- * Required: `<NAME>`, optional: `[NAME]`, variadic: `[NAME]...`.
64
- */
65
- label: Uppercase<string>;
66
- };
67
-
68
40
  /**
69
41
  * Creates a required positional — missing token throws {@link TypoError}.
70
- * Label defaults to uppercased `type.content` in angle brackets (e.g. `<STRING>`).
71
42
  *
72
43
  * @typeParam Value - Type produced by the decoder.
73
44
  *
74
- * @param definition - Positional configuration.
75
45
  * @param definition.description - Help text.
76
46
  * @param definition.hint - Short note shown in parentheses.
77
- * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
78
47
  * @param definition.type - Decoder for the raw token.
79
48
  * @returns A {@link Positional}`<Value>`.
80
49
  *
81
50
  * @example
82
51
  * ```ts
83
52
  * const namePositional = positionalRequired({
84
- * type: typeString,
85
- * label: "NAME",
53
+ * type: type("name"),
86
54
  * description: "The name to greet",
87
55
  * });
88
- * // Parses: my-cli Alice → "Alice"
56
+ * // Usage:
57
+ * // my-cli Alice → "Alice"
89
58
  * ```
90
59
  */
91
60
  export function positionalRequired<Value>(definition: {
92
61
  description?: string;
93
62
  hint?: string;
94
- label?: Uppercase<string>;
95
63
  type: Type<Value>;
96
64
  }): Positional<Value> {
97
- const label = `<${definition.label ?? definition.type.content.toUpperCase()}>`;
65
+ const { description, hint, type } = definition;
66
+ const label = `<${type.content}>`;
98
67
  return {
99
68
  generateUsage() {
100
- return {
101
- description: definition.description,
102
- hint: definition.hint,
103
- label: label as Uppercase<string>,
104
- };
69
+ return { description, hint, label };
105
70
  },
106
71
  consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
107
72
  const positional = readerPositionals.consumePositional();
@@ -124,14 +89,11 @@ export function positionalRequired<Value>(definition: {
124
89
 
125
90
  /**
126
91
  * Creates an optional positional — absent token falls back to `default()`.
127
- * Label defaults to uppercased `type.content` in square brackets (e.g. `[STRING]`).
128
92
  *
129
93
  * @typeParam Value - Type produced by the decoder (or the default).
130
94
  *
131
- * @param definition - Positional configuration.
132
95
  * @param definition.description - Help text.
133
96
  * @param definition.hint - Short note shown in parentheses.
134
- * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
135
97
  * @param definition.type - Decoder for the raw token.
136
98
  * @param definition.default - Value when absent. Throw to make it required.
137
99
  * @returns A {@link Positional}`<Value>`.
@@ -139,30 +101,26 @@ export function positionalRequired<Value>(definition: {
139
101
  * @example
140
102
  * ```ts
141
103
  * const greeteePositional = positionalOptional({
142
- * type: typeString,
143
- * label: "NAME",
104
+ * type: type("name"),
144
105
  * description: "Name to greet (default: world)",
145
106
  * default: () => "world",
146
107
  * });
147
- * // my-cli → "world"
148
- * // my-cli Alice → "Alice"
108
+ * // Usage:
109
+ * // my-cli → "world"
110
+ * // my-cli Alice → "Alice"
149
111
  * ```
150
112
  */
151
113
  export function positionalOptional<Value>(definition: {
152
114
  description?: string;
153
115
  hint?: string;
154
- label?: Uppercase<string>;
155
116
  type: Type<Value>;
156
117
  default: () => Value;
157
118
  }): Positional<Value> {
158
- const label = `[${definition.label ?? definition.type.content.toUpperCase()}]`;
119
+ const { description, hint, type } = definition;
120
+ const label = `[${type.content}]`;
159
121
  return {
160
122
  generateUsage() {
161
- return {
162
- description: definition.description,
163
- hint: definition.hint,
164
- label: label as Uppercase<string>,
165
- };
123
+ return { description, hint, label };
166
124
  },
167
125
  consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
168
126
  const positional = readerPositionals.consumePositional();
@@ -172,13 +130,7 @@ export function positionalOptional<Value>(definition: {
172
130
  try {
173
131
  return definition.default();
174
132
  } catch (error) {
175
- throw new TypoError(
176
- new TypoText(
177
- new TypoString(label, typoStyleUserInput),
178
- new TypoString(`: Failed to get default value`),
179
- ),
180
- error,
181
- );
133
+ throwsWhenFailedToGetDefault(label);
182
134
  }
183
135
  }
184
136
  return decodeValue(label, definition.type, positional);
@@ -190,47 +142,41 @@ export function positionalOptional<Value>(definition: {
190
142
 
191
143
  /**
192
144
  * Creates a variadic positional that collects zero or more remaining tokens into an array.
193
- * Stops at `endDelimiter` (consumed, not included). Label: `[TYPE]...` notation.
145
+ * Optionally stops at `endDelimiter` (consumed, not included).
194
146
  *
195
147
  * @typeParam Value - Type produced by the decoder for each token.
196
148
  *
197
- * @param definition - Positional configuration.
198
149
  * @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
199
150
  * @param definition.description - Help text.
200
151
  * @param definition.hint - Short note shown in parentheses.
201
- * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
202
152
  * @param definition.type - Decoder applied to each token.
203
153
  * @returns A {@link Positional}`<Array<Value>>`.
204
154
  *
205
155
  * @example
206
156
  * ```ts
207
157
  * const filesPositional = positionalVariadics({
208
- * type: typeString,
209
- * label: "FILE",
158
+ * type: typePath(),
210
159
  * description: "Files to process",
211
160
  * });
212
- * // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
213
- * // my-cli → []
161
+ * // Usage:
162
+ * // my-cli → []
163
+ * // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
214
164
  * ```
215
165
  */
216
166
  export function positionalVariadics<Value>(definition: {
217
167
  endDelimiter?: string;
218
168
  description?: string;
219
169
  hint?: string;
220
- label?: Uppercase<string>;
221
170
  type: Type<Value>;
222
171
  }): Positional<Array<Value>> {
223
- const label = `[${definition.label ?? definition.type.content.toUpperCase()}]`;
172
+ const { description, hint, type } = definition;
173
+ const labelSingle = `[${type.content}]`;
174
+ const labelMultiple =
175
+ `${labelSingle}...` +
176
+ (definition.endDelimiter ? ` ["${definition.endDelimiter}"]` : "");
224
177
  return {
225
178
  generateUsage() {
226
- return {
227
- description: definition.description,
228
- hint: definition.hint,
229
- label: (`${label}...` +
230
- (definition.endDelimiter
231
- ? `["${definition.endDelimiter}"]`
232
- : "")) as Uppercase<string>,
233
- };
179
+ return { description, hint, label: labelMultiple };
234
180
  },
235
181
  consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
236
182
  const positionals = new Array<string>();
@@ -246,9 +192,9 @@ export function positionalVariadics<Value>(definition: {
246
192
  }
247
193
  return {
248
194
  decodeValue() {
249
- return positionals.map((positional) => {
250
- return decodeValue(label, definition.type, positional);
251
- });
195
+ return positionals.map((positional) =>
196
+ decodeValue(labelSingle, definition.type, positional),
197
+ );
252
198
  },
253
199
  };
254
200
  },
@@ -262,11 +208,15 @@ function decodeValue<Value>(
262
208
  ): Value {
263
209
  return TypoError.tryWithContext(
264
210
  () => type.decoder(input),
265
- () =>
266
- new TypoText(
267
- new TypoString(label, typoStyleUserInput),
268
- new TypoString(`: `),
269
- new TypoString(type.content, typoStyleLogic),
270
- ),
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
+ ),
271
221
  );
272
222
  }
package/src/lib/Reader.ts CHANGED
@@ -7,44 +7,58 @@ import {
7
7
  } from "./Typo";
8
8
 
9
9
  /**
10
- * Opaque key identifying a registered option within a {@link ReaderArgs} instance.
11
- * Returned by {@link ReaderArgs.registerOption}; passed to {@link ReaderArgs.getOptionValues}.
10
+ * Opaque key returned by {@link ReaderArgs.registerOption}.
12
11
  */
13
12
  export type ReaderOptionKey = (string | { __brand: "ReaderOptionKey" }) & {
14
13
  __brand: "ReaderOptionKey";
15
14
  };
16
15
 
17
16
  /**
18
- * Option registration and query interface, implemented by {@link ReaderArgs}.
19
- * Exposed separately from {@link ReaderPositionals} so parsers depend only on what they need.
17
+ * Parsing behaviour for a registered option, passed to {@link ReaderArgs.registerOption}.
18
+ */
19
+ export type ReaderOptionParsing = {
20
+ consumeShortGroup: boolean; // TODO - this doesnt matter when no short option
21
+ consumeNextArg: (
22
+ inlined: string | null,
23
+ separated: Array<string>,
24
+ nextArg: string | undefined,
25
+ ) => boolean;
26
+ };
27
+
28
+ /**
29
+ * Result of parsing an option, including its inlined value and any following separated values.
30
+ */
31
+ export type ReaderOptionValue = {
32
+ inlined: string | null;
33
+ separated: Array<string>;
34
+ };
35
+
36
+ /**
37
+ * Option registration/query interface. Subset of {@link ReaderArgs}.
20
38
  */
21
39
  export type ReaderOptions = {
22
40
  /**
23
- * Registers an option so the parser can recognise it.
41
+ * Registers an option; all `longs` and `shorts` share the same key.
24
42
  *
25
43
  * @param definition.longs - Long-form names (without `--`).
26
44
  * @param definition.shorts - Short-form names (without `-`).
27
- * @param definition.valued - `true` if the option takes a value; `false` for flags.
45
+ * @param definition.parsing - Parsing behaviour.
28
46
  * @returns A {@link ReaderOptionKey} for later retrieval.
29
47
  * @throws `Error` if a name is already registered or short names overlap.
30
48
  */
31
49
  registerOption(definition: {
32
50
  longs: Array<string>;
33
51
  shorts: Array<string>;
34
- valued: boolean;
52
+ parsing: ReaderOptionParsing;
35
53
  }): ReaderOptionKey;
36
54
  /**
37
- * Returns all values collected for the option identified by `key`.
38
- *
39
- * @param key - Key from {@link ReaderOptions.registerOption}.
40
- * @returns Raw string values, one per occurrence; empty if never provided.
41
- * @throws `Error` if `key` was not registered.
55
+ * Returns all values collected for `key`.
42
56
  */
43
- getOptionValues(key: ReaderOptionKey): Array<string>;
57
+ getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue>;
44
58
  };
45
59
 
46
60
  /**
47
- * Positional token consumption interface, implemented by {@link ReaderArgs}.
61
+ * Positional consumption interface. Subset of {@link ReaderArgs}.
48
62
  */
49
63
  export type ReaderPositionals = {
50
64
  /**
@@ -69,10 +83,9 @@ export class ReaderArgs {
69
83
  #args: ReadonlyArray<string>;
70
84
  #parsedIndex: number;
71
85
  #parsedDouble: boolean;
72
- #keyByLong: Map<string, ReaderOptionKey>;
73
- #keyByShort: Map<string, ReaderOptionKey>;
74
- #valuedByKey: Map<ReaderOptionKey, boolean>;
75
- #resultByKey: Map<ReaderOptionKey, Array<string>>;
86
+ #optionContextByLong: Map<string, ReaderOptionContext>;
87
+ #optionContextByShort: Map<string, ReaderOptionContext>;
88
+ #optionContextByKey: Map<ReaderOptionKey, ReaderOptionContext>;
76
89
 
77
90
  /**
78
91
  * @param args - Raw CLI tokens (e.g. `process.argv.slice(2)`). Not mutated.
@@ -81,27 +94,25 @@ export class ReaderArgs {
81
94
  this.#args = args;
82
95
  this.#parsedIndex = 0;
83
96
  this.#parsedDouble = false;
84
- this.#keyByLong = new Map();
85
- this.#keyByShort = new Map();
86
- this.#valuedByKey = new Map();
87
- this.#resultByKey = new Map();
97
+ this.#optionContextByLong = new Map();
98
+ this.#optionContextByShort = new Map();
99
+ this.#optionContextByKey = new Map();
88
100
  }
89
101
 
90
102
  /**
91
103
  * Registers an option; all `longs` and `shorts` share the same key.
92
- * Short names support stacking (e.g. `-abc`) and inline values (e.g. `-nvalue`),
93
- * but must not be prefixes of one another.
104
+ * Short names must not be prefixes of one another.
94
105
  *
95
106
  * @param definition.longs - Long-form names (without `--`).
96
107
  * @param definition.shorts - Short-form names (without `-`).
97
- * @param definition.valued - `true` if the option takes a value; `false` for flags.
108
+ * @param definition.parsing - Parsing behaviour.
98
109
  * @returns A {@link ReaderOptionKey} for {@link ReaderArgs.getOptionValues}.
99
110
  * @throws `Error` if any name is already registered or short names overlap.
100
111
  */
101
112
  registerOption(definition: {
102
113
  longs: Array<string>;
103
114
  shorts: Array<string>;
104
- valued: boolean;
115
+ parsing: ReaderOptionParsing;
105
116
  }) {
106
117
  const key = [
107
118
  ...definition.longs.map((long) => `--${long}`),
@@ -111,7 +122,7 @@ export class ReaderArgs {
111
122
  if (!this.#isValidOptionName(long)) {
112
123
  throw new Error(`Invalid option name: --${long}`);
113
124
  }
114
- if (this.#keyByLong.has(long)) {
125
+ if (this.#optionContextByLong.has(long)) {
115
126
  throw new Error(`Option already registered: --${long}`);
116
127
  }
117
128
  }
@@ -119,18 +130,18 @@ export class ReaderArgs {
119
130
  if (!this.#isValidOptionName(short)) {
120
131
  throw new Error(`Invalid option name: -${short}`);
121
132
  }
122
- if (this.#keyByShort.has(short)) {
133
+ if (this.#optionContextByShort.has(short)) {
123
134
  throw new Error(`Option already registered: -${short}`);
124
135
  }
125
136
  for (let i = 0; i < short.length; i++) {
126
137
  const shortSlice = short.slice(0, i);
127
- if (this.#keyByShort.has(shortSlice)) {
138
+ if (this.#optionContextByShort.has(shortSlice)) {
128
139
  throw new Error(
129
140
  `Option -${short} overlap with shorter option: -${shortSlice}`,
130
141
  );
131
142
  }
132
143
  }
133
- for (const shortOther of this.#keyByShort.keys()) {
144
+ for (const shortOther of this.#optionContextByShort.keys()) {
134
145
  if (shortOther.startsWith(short)) {
135
146
  throw new Error(
136
147
  `Option -${short} overlap with longer option: -${shortOther}`,
@@ -138,35 +149,37 @@ export class ReaderArgs {
138
149
  }
139
150
  }
140
151
  }
152
+ const optionContext = {
153
+ parsing: definition.parsing,
154
+ results: new Array<ReaderOptionValue>(),
155
+ };
141
156
  for (const long of definition.longs) {
142
- this.#keyByLong.set(long, key);
157
+ this.#optionContextByLong.set(long, optionContext);
143
158
  }
144
159
  for (const short of definition.shorts) {
145
- this.#keyByShort.set(short, key);
160
+ this.#optionContextByShort.set(short, optionContext);
146
161
  }
147
- this.#valuedByKey.set(key, definition.valued);
148
- this.#resultByKey.set(key, new Array<string>());
162
+ this.#optionContextByKey.set(key, optionContext);
149
163
  return key;
150
164
  }
151
165
 
152
166
  /**
153
- * Returns all values collected for the option key.
167
+ * Returns all values collected for `key`.
154
168
  *
155
169
  * @param key - Key from {@link ReaderArgs.registerOption}.
156
- * @returns String values, one per occurrence.
170
+ * @returns One entry per occurrence.
157
171
  * @throws `Error` if `key` was not registered.
158
172
  */
159
- getOptionValues(key: ReaderOptionKey): Array<string> {
160
- const optionResult = this.#resultByKey.get(key);
161
- if (optionResult === undefined) {
173
+ getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue> {
174
+ const optionContext = this.#optionContextByKey.get(key);
175
+ if (optionContext === undefined) {
162
176
  throw new Error(`Unregistered option: ${key}`);
163
177
  }
164
- return optionResult;
178
+ return optionContext.results;
165
179
  }
166
180
 
167
181
  /**
168
- * Returns the next bare positional token.
169
- * Parse intervening options as side-effects.
182
+ * Returns the next positional token; parses intervening options as a side-effect.
170
183
  * All tokens after `--` are treated as positionals.
171
184
  *
172
185
  * @returns The next positional, or `undefined` when exhausted.
@@ -175,19 +188,19 @@ export class ReaderArgs {
175
188
  consumePositional(): string | undefined {
176
189
  while (true) {
177
190
  const arg = this.#consumeArg();
178
- if (arg === null) {
191
+ if (arg === undefined) {
179
192
  return undefined;
180
193
  }
181
- if (this.#processedAsPositional(arg)) {
194
+ if (!this.#tryConsumeAsOption(arg)) {
182
195
  return arg;
183
196
  }
184
197
  }
185
198
  }
186
199
 
187
- #consumeArg(): string | null {
200
+ #consumeArg(): string | undefined {
188
201
  const arg = this.#args[this.#parsedIndex];
189
202
  if (arg === undefined) {
190
- return null;
203
+ return undefined;
191
204
  }
192
205
  this.#parsedIndex++;
193
206
  if (!this.#parsedDouble) {
@@ -199,9 +212,9 @@ export class ReaderArgs {
199
212
  return arg;
200
213
  }
201
214
 
202
- #processedAsPositional(arg: string): boolean {
215
+ #tryConsumeAsOption(arg: string): boolean {
203
216
  if (this.#parsedDouble) {
204
- return true;
217
+ return false;
205
218
  }
206
219
  if (arg.startsWith("--")) {
207
220
  const valueIndexStart = arg.indexOf("=");
@@ -213,80 +226,90 @@ export class ReaderArgs {
213
226
  arg.slice(valueIndexStart + 1),
214
227
  );
215
228
  }
216
- return false;
229
+ return true;
217
230
  }
218
231
  if (arg.startsWith("-")) {
219
232
  let shortIndexStart = 1;
220
233
  let shortIndexEnd = 2;
221
234
  while (shortIndexEnd <= arg.length) {
222
- const result = this.#tryConsumeOptionShort(
223
- arg.slice(shortIndexStart, shortIndexEnd),
224
- arg.slice(shortIndexEnd),
225
- );
226
- if (result === true) {
227
- return false;
228
- }
229
- if (result === false) {
235
+ const short = arg.slice(shortIndexStart, shortIndexEnd);
236
+ const optionContext = this.#optionContextByShort.get(short);
237
+ if (optionContext !== undefined) {
238
+ const rest = arg.slice(shortIndexEnd);
239
+ if (this.#tryConsumeOptionShort(optionContext, short, rest)) {
240
+ return true;
241
+ }
230
242
  shortIndexStart = shortIndexEnd;
231
243
  }
232
244
  shortIndexEnd++;
233
245
  }
234
246
  throw new TypoError(
235
247
  new TypoText(
236
- new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleConstants),
237
- new TypoString(`: Unexpected unknown option`),
248
+ new TypoString(`Unexpected unknown option(s): `),
249
+ new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleQuote),
238
250
  ),
239
251
  );
240
252
  }
241
- return true;
253
+ return false;
242
254
  }
243
255
 
244
- #consumeOptionLong(long: string, direct: string | null): void {
256
+ #consumeOptionLong(long: string, inlined: string | null): void {
245
257
  const constant = `--${long}`;
246
- const key = this.#keyByLong.get(long);
247
- if (key !== undefined) {
248
- if (direct !== null) {
249
- return this.#acknowledgeOption(key, direct);
250
- }
251
- const valued = this.#valuedByKey.get(key);
252
- if (valued) {
253
- return this.#acknowledgeOption(key, this.#consumeOptionValue(constant));
254
- }
255
- return this.#acknowledgeOption(key, "true");
258
+ const optionContext = this.#optionContextByLong.get(long);
259
+ if (optionContext !== undefined) {
260
+ return this.#consumeOptionValues(optionContext, constant, inlined);
256
261
  }
257
262
  throw new TypoError(
258
263
  new TypoText(
259
- new TypoString(constant, typoStyleConstants),
260
- new TypoString(`: Unexpected unknown option`),
264
+ new TypoString(`Unexpected unknown option: `),
265
+ new TypoString(constant, typoStyleQuote),
261
266
  ),
262
267
  );
263
268
  }
264
269
 
265
- #tryConsumeOptionShort(short: string, rest: string): boolean | null {
266
- const key = this.#keyByShort.get(short);
267
- if (key !== undefined) {
268
- if (rest.startsWith("=")) {
269
- this.#acknowledgeOption(key, rest.slice(1));
270
- return true;
271
- }
272
- const valued = this.#valuedByKey.get(key);
273
- if (valued) {
274
- if (rest === "") {
275
- this.#acknowledgeOption(key, this.#consumeOptionValue(`-${short}`));
276
- } else {
277
- this.#acknowledgeOption(key, rest);
278
- }
279
- return true;
280
- }
281
- this.#acknowledgeOption(key, "true");
282
- return rest === "";
270
+ #tryConsumeOptionShort(
271
+ optionContext: ReaderOptionContext,
272
+ short: string,
273
+ rest: string,
274
+ ): boolean {
275
+ const constant = `-${short}`;
276
+ if (rest.startsWith("=")) {
277
+ this.#consumeOptionValues(optionContext, constant, rest.slice(1));
278
+ return true;
279
+ }
280
+ if (rest.length === 0) {
281
+ this.#consumeOptionValues(optionContext, constant, null);
282
+ return true;
283
+ }
284
+ if (optionContext.parsing.consumeShortGroup) {
285
+ this.#consumeOptionValues(optionContext, constant, rest);
286
+ return true;
283
287
  }
284
- return null;
288
+ this.#consumeOptionValues(optionContext, constant, null);
289
+ return false;
285
290
  }
286
291
 
287
- #consumeOptionValue(constant: string) {
292
+ #consumeOptionValues(
293
+ optionContext: ReaderOptionContext,
294
+ constant: string,
295
+ inlined: string | null,
296
+ ) {
297
+ const separated = new Array<string>();
298
+ while (
299
+ optionContext.parsing.consumeNextArg(
300
+ inlined,
301
+ separated,
302
+ this.#args[this.#parsedIndex],
303
+ )
304
+ ) {
305
+ separated.push(this.#consumeOptionValue(constant));
306
+ }
307
+ optionContext.results.push({ inlined, separated });
308
+ }
309
+
310
+ #consumeOptionValue(constant: string): string {
288
311
  const arg = this.#consumeArg();
289
- if (arg === null) {
312
+ if (arg === undefined) {
290
313
  throw new TypoError(
291
314
  new TypoText(
292
315
  new TypoString(constant, typoStyleConstants),
@@ -316,11 +339,12 @@ export class ReaderArgs {
316
339
  return arg;
317
340
  }
318
341
 
319
- #acknowledgeOption(key: ReaderOptionKey, value: string) {
320
- this.getOptionValues(key).push(value);
321
- }
322
-
323
342
  #isValidOptionName(name: string): boolean {
324
- return name.length > 0 && !name.includes("=");
343
+ return name.length > 0 && !name.includes("=") && !name.includes("\0");
325
344
  }
326
345
  }
346
+
347
+ type ReaderOptionContext = {
348
+ parsing: ReaderOptionParsing;
349
+ results: Array<ReaderOptionValue>;
350
+ };