cli-kiss 0.2.6 → 0.2.8
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 +62 -4
- package/dist/index.d.ts +135 -128
- 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/02_commands.md +1 -1
- package/docs/guide/03_options.md +11 -11
- package/docs/guide/05_input_types.md +9 -10
- 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 -0
- package/src/lib/Command.ts +50 -30
- package/src/lib/Operation.ts +29 -21
- package/src/lib/Option.ts +198 -133
- package/src/lib/Positional.ts +46 -24
- package/src/lib/Reader.ts +194 -207
- package/src/lib/Run.ts +19 -8
- package/src/lib/Suggest.ts +78 -0
- package/src/lib/Type.ts +46 -48
- package/src/lib/Typo.ts +72 -47
- package/src/lib/Usage.ts +13 -13
- package/tests/unit.Reader.commons.ts +92 -116
- package/tests/unit.Reader.parsings.ts +14 -26
- package/tests/unit.Reader.shortBig.ts +81 -96
- package/tests/unit.command.aliases.ts +100 -0
- package/tests/unit.command.execute.ts +1 -1
- package/tests/unit.command.usage.ts +12 -6
- package/tests/unit.fuzzed.alternatives.ts +43 -0
- package/tests/unit.runner.colors.ts +11 -35
- package/tests/unit.runner.cycle.ts +181 -128
- package/tests/unit.runner.errors.ts +26 -19
- package/docs/public/hero.png +0 -0
- package/tests/unit.Reader.aliases.ts +0 -62
package/src/lib/Reader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { suggestTextPushMessage } from "./Suggest";
|
|
1
2
|
import {
|
|
2
3
|
TypoError,
|
|
3
4
|
TypoString,
|
|
@@ -7,54 +8,55 @@ import {
|
|
|
7
8
|
} from "./Typo";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Opaque key returned by {@link ReaderArgs.registerOption}.
|
|
11
11
|
*/
|
|
12
|
-
export type
|
|
13
|
-
|
|
12
|
+
export type ReaderOptionLongSpec = {
|
|
13
|
+
key: string;
|
|
14
|
+
nextGuard: ReaderOptionNextGuard;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Parsing behaviour for a registered option, passed to {@link ReaderArgs.registerOption}.
|
|
18
18
|
*/
|
|
19
|
-
export type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
export type ReaderOptionShortSpec = {
|
|
20
|
+
key: string;
|
|
21
|
+
restGuard: ReaderOptionRestGuard;
|
|
22
|
+
nextGuard: ReaderOptionNextGuard;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
*/
|
|
27
|
+
export type ReaderOptionRestGuard = (rest: string) => boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
*/
|
|
31
|
+
export type ReaderOptionNextGuard = (
|
|
32
|
+
value: ReaderOptionValue,
|
|
33
|
+
next: string | undefined,
|
|
34
|
+
) => boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*/
|
|
38
|
+
export type ReaderOptionResult = {
|
|
39
|
+
identifier: string;
|
|
40
|
+
values: ReadonlyArray<ReaderOptionValue>;
|
|
26
41
|
};
|
|
27
42
|
|
|
28
43
|
/**
|
|
29
|
-
* Result of parsing an option, including its inlined value and any following separated values.
|
|
30
44
|
*/
|
|
31
45
|
export type ReaderOptionValue = {
|
|
32
46
|
inlined: string | null;
|
|
33
|
-
separated:
|
|
47
|
+
separated: ReadonlyArray<string>;
|
|
34
48
|
};
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
*/
|
|
52
|
+
export type ReaderOptionGetter = () => ReaderOptionResult;
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* Option registration/query interface. Subset of {@link ReaderArgs}.
|
|
38
56
|
*/
|
|
39
57
|
export type ReaderOptions = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* @param definition.longs - Long-form names (without `--`).
|
|
44
|
-
* @param definition.shorts - Short-form names (without `-`).
|
|
45
|
-
* @param definition.parsing - Parsing behaviour.
|
|
46
|
-
* @returns A {@link ReaderOptionKey} for later retrieval.
|
|
47
|
-
* @throws `Error` if a name is already registered or short names overlap.
|
|
48
|
-
*/
|
|
49
|
-
registerOption(definition: {
|
|
50
|
-
longs: Array<string>;
|
|
51
|
-
shorts: Array<string>;
|
|
52
|
-
parsing: ReaderOptionParsing;
|
|
53
|
-
}): ReaderOptionKey;
|
|
54
|
-
/**
|
|
55
|
-
* Returns all values collected for `key`.
|
|
56
|
-
*/
|
|
57
|
-
getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue>;
|
|
58
|
+
registerOptionLong(longSpec: ReaderOptionLongSpec): ReaderOptionGetter;
|
|
59
|
+
registerOptionShort(shortSpec: ReaderOptionShortSpec): ReaderOptionGetter;
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -65,7 +67,7 @@ export type ReaderPositionals = {
|
|
|
65
67
|
* Returns the next positional token, parsing intervening options as side-effects.
|
|
66
68
|
*
|
|
67
69
|
* @returns The next positional, or `undefined` when exhausted.
|
|
68
|
-
* @throws
|
|
70
|
+
* @throws on an unrecognised option.
|
|
69
71
|
*/
|
|
70
72
|
consumePositional(): string | undefined;
|
|
71
73
|
};
|
|
@@ -80,102 +82,74 @@ export type ReaderPositionals = {
|
|
|
80
82
|
* Created internally by {@link runAndExit}; exposed for advanced / custom runners.
|
|
81
83
|
*/
|
|
82
84
|
export class ReaderArgs {
|
|
83
|
-
#
|
|
85
|
+
#tokens: ReadonlyArray<string>;
|
|
84
86
|
#parsedIndex: number;
|
|
85
87
|
#parsedDouble: boolean;
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#optionContextByKey: Map<ReaderOptionKey, ReaderOptionContext>;
|
|
88
|
+
#optionLongContextByIdentifier: Map<string, Context<ReaderOptionLongSpec>>;
|
|
89
|
+
#optionShortContextByIdentifier: Map<string, Context<ReaderOptionShortSpec>>;
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* @param
|
|
92
|
+
* @param tokens - Raw CLI tokens (e.g. `process.argv.slice(2)`).
|
|
92
93
|
*/
|
|
93
|
-
constructor(
|
|
94
|
-
this.#
|
|
94
|
+
constructor(tokens: ReadonlyArray<string>) {
|
|
95
|
+
this.#tokens = tokens;
|
|
95
96
|
this.#parsedIndex = 0;
|
|
96
97
|
this.#parsedDouble = false;
|
|
97
|
-
this.#
|
|
98
|
-
this.#
|
|
99
|
-
this.#optionContextByKey = new Map();
|
|
98
|
+
this.#optionLongContextByIdentifier = new Map();
|
|
99
|
+
this.#optionShortContextByIdentifier = new Map();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Registers an option; all `longs` and `shorts` share the same key.
|
|
104
|
-
* Short names must not be prefixes of one another.
|
|
105
|
-
*
|
|
106
|
-
* @param definition.longs - Long-form names (without `--`).
|
|
107
|
-
* @param definition.shorts - Short-form names (without `-`).
|
|
108
|
-
* @param definition.parsing - Parsing behaviour.
|
|
109
|
-
* @returns A {@link ReaderOptionKey} for {@link ReaderArgs.getOptionValues}.
|
|
110
|
-
* @throws `Error` if any name is already registered or short names overlap.
|
|
111
103
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}) {
|
|
117
|
-
const key = [
|
|
118
|
-
...definition.longs.map((long) => `--${long}`),
|
|
119
|
-
...definition.shorts.map((short) => `-${short}`),
|
|
120
|
-
].join(", ") as ReaderOptionKey;
|
|
121
|
-
for (const long of definition.longs) {
|
|
122
|
-
if (!this.#isValidOptionName(long)) {
|
|
123
|
-
throw new Error(`Invalid option name: --${long}`);
|
|
124
|
-
}
|
|
125
|
-
if (this.#optionContextByLong.has(long)) {
|
|
126
|
-
throw new Error(`Option already registered: --${long}`);
|
|
127
|
-
}
|
|
104
|
+
registerOptionLong(longSpec: ReaderOptionLongSpec): ReaderOptionGetter {
|
|
105
|
+
const identifier = `--${longSpec.key}`;
|
|
106
|
+
if (!isValidOptionKey(longSpec.key)) {
|
|
107
|
+
throw new Error(`Option identifier is invalid: ${identifier}.`);
|
|
128
108
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
throw new Error(`Invalid option name: -${short}`);
|
|
132
|
-
}
|
|
133
|
-
if (this.#optionContextByShort.has(short)) {
|
|
134
|
-
throw new Error(`Option already registered: -${short}`);
|
|
135
|
-
}
|
|
136
|
-
for (let i = 0; i < short.length; i++) {
|
|
137
|
-
const shortSlice = short.slice(0, i);
|
|
138
|
-
if (this.#optionContextByShort.has(shortSlice)) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`Option -${short} overlap with shorter option: -${shortSlice}`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
for (const shortOther of this.#optionContextByShort.keys()) {
|
|
145
|
-
if (shortOther.startsWith(short)) {
|
|
146
|
-
throw new Error(
|
|
147
|
-
`Option -${short} overlap with longer option: -${shortOther}`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const optionContext = {
|
|
153
|
-
parsing: definition.parsing,
|
|
154
|
-
results: new Array<ReaderOptionValue>(),
|
|
155
|
-
};
|
|
156
|
-
for (const long of definition.longs) {
|
|
157
|
-
this.#optionContextByLong.set(long, optionContext);
|
|
109
|
+
if (this.#optionLongContextByIdentifier.has(identifier)) {
|
|
110
|
+
throw new Error(`Option already registered: ${identifier}.`);
|
|
158
111
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
112
|
+
const values = new Array<ReaderOptionValue>();
|
|
113
|
+
this.#optionLongContextByIdentifier.set(identifier, {
|
|
114
|
+
identifier,
|
|
115
|
+
spec: longSpec,
|
|
116
|
+
values,
|
|
117
|
+
});
|
|
118
|
+
return () => ({ identifier, values });
|
|
164
119
|
}
|
|
165
120
|
|
|
166
121
|
/**
|
|
167
|
-
* Returns all values collected for `key`.
|
|
168
|
-
*
|
|
169
|
-
* @param key - Key from {@link ReaderArgs.registerOption}.
|
|
170
|
-
* @returns One entry per occurrence.
|
|
171
|
-
* @throws `Error` if `key` was not registered.
|
|
172
122
|
*/
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
throw new Error(`
|
|
123
|
+
registerOptionShort(shortSpec: ReaderOptionShortSpec): ReaderOptionGetter {
|
|
124
|
+
const identifier = `-${shortSpec.key}`;
|
|
125
|
+
if (!isValidOptionKey(shortSpec.key)) {
|
|
126
|
+
throw new Error(`Option identifier is invalid: ${identifier}.`);
|
|
177
127
|
}
|
|
178
|
-
|
|
128
|
+
if (this.#optionShortContextByIdentifier.has(identifier)) {
|
|
129
|
+
throw new Error(`Option already registered: ${identifier}.`);
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
132
|
+
const slicedIdentifier = identifier.slice(0, 1 + i);
|
|
133
|
+
if (this.#optionShortContextByIdentifier.has(slicedIdentifier)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Option ${identifier} overlap with shorter option: ${slicedIdentifier}.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const otherIdentifier of this.#optionShortContextByIdentifier.keys()) {
|
|
140
|
+
if (otherIdentifier.startsWith(identifier)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Option ${identifier} overlap with longer option: ${otherIdentifier}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const values = new Array<ReaderOptionValue>();
|
|
147
|
+
this.#optionShortContextByIdentifier.set(identifier, {
|
|
148
|
+
identifier,
|
|
149
|
+
spec: shortSpec,
|
|
150
|
+
values,
|
|
151
|
+
});
|
|
152
|
+
return () => ({ identifier, values });
|
|
179
153
|
}
|
|
180
154
|
|
|
181
155
|
/**
|
|
@@ -183,168 +157,181 @@ export class ReaderArgs {
|
|
|
183
157
|
* All tokens after `--` are treated as positionals.
|
|
184
158
|
*
|
|
185
159
|
* @returns The next positional, or `undefined` when exhausted.
|
|
186
|
-
* @throws
|
|
160
|
+
* @throws on an unrecognised option.
|
|
187
161
|
*/
|
|
188
162
|
consumePositional(): string | undefined {
|
|
189
163
|
while (true) {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
164
|
+
const token = this.#consumeToken();
|
|
165
|
+
if (token === undefined) {
|
|
192
166
|
return undefined;
|
|
193
167
|
}
|
|
194
|
-
if (!this.#tryConsumeAsOption(
|
|
195
|
-
return
|
|
168
|
+
if (!this.#tryConsumeAsOption(token)) {
|
|
169
|
+
return token;
|
|
196
170
|
}
|
|
197
171
|
}
|
|
198
172
|
}
|
|
199
173
|
|
|
200
|
-
#
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
174
|
+
#consumeToken(): string | undefined {
|
|
175
|
+
const token = this.#tokens[this.#parsedIndex];
|
|
176
|
+
if (token === undefined) {
|
|
203
177
|
return undefined;
|
|
204
178
|
}
|
|
205
179
|
this.#parsedIndex++;
|
|
206
180
|
if (!this.#parsedDouble) {
|
|
207
|
-
if (
|
|
181
|
+
if (token === "--") {
|
|
208
182
|
this.#parsedDouble = true;
|
|
209
|
-
return this.#
|
|
183
|
+
return this.#consumeToken();
|
|
210
184
|
}
|
|
211
185
|
}
|
|
212
|
-
return
|
|
186
|
+
return token;
|
|
213
187
|
}
|
|
214
188
|
|
|
215
|
-
#tryConsumeAsOption(
|
|
189
|
+
#tryConsumeAsOption(token: string): boolean {
|
|
216
190
|
if (this.#parsedDouble) {
|
|
217
191
|
return false;
|
|
218
192
|
}
|
|
219
|
-
if (
|
|
220
|
-
const valueIndexStart =
|
|
193
|
+
if (token.startsWith("--")) {
|
|
194
|
+
const valueIndexStart = token.indexOf("=");
|
|
221
195
|
if (valueIndexStart === -1) {
|
|
222
|
-
this.#consumeOptionLong(
|
|
196
|
+
this.#consumeOptionLong(token, null);
|
|
223
197
|
} else {
|
|
224
198
|
this.#consumeOptionLong(
|
|
225
|
-
|
|
226
|
-
|
|
199
|
+
token.slice(0, valueIndexStart),
|
|
200
|
+
token.slice(valueIndexStart + 1),
|
|
227
201
|
);
|
|
228
202
|
}
|
|
229
203
|
return true;
|
|
230
204
|
}
|
|
231
|
-
if (
|
|
205
|
+
if (token.startsWith("-")) {
|
|
232
206
|
let shortIndexStart = 1;
|
|
233
207
|
let shortIndexEnd = 2;
|
|
234
|
-
while (shortIndexEnd <=
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
208
|
+
while (shortIndexEnd <= token.length) {
|
|
209
|
+
const identifier = `-${token.slice(shortIndexStart, shortIndexEnd)}`;
|
|
210
|
+
const shortContext =
|
|
211
|
+
this.#optionShortContextByIdentifier.get(identifier);
|
|
212
|
+
if (shortContext !== undefined) {
|
|
213
|
+
const tokenRest = token.slice(shortIndexEnd);
|
|
214
|
+
if (this.#tryConsumeOptionShort(shortContext, tokenRest)) {
|
|
240
215
|
return true;
|
|
241
216
|
}
|
|
242
217
|
shortIndexStart = shortIndexEnd;
|
|
243
218
|
}
|
|
244
219
|
shortIndexEnd++;
|
|
245
220
|
}
|
|
246
|
-
|
|
247
|
-
new TypoText(
|
|
248
|
-
new TypoString(`Unexpected unknown option(s): `),
|
|
249
|
-
new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleQuote),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
221
|
+
this.#throwUnknownOptionError(`-${token.slice(shortIndexStart)}`);
|
|
252
222
|
}
|
|
253
223
|
return false;
|
|
254
224
|
}
|
|
255
225
|
|
|
256
|
-
#consumeOptionLong(
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return this.#consumeOptionValues(optionContext, constant, inlined);
|
|
226
|
+
#consumeOptionLong(identifier: string, valueInlined: string | null): void {
|
|
227
|
+
const longContext = this.#optionLongContextByIdentifier.get(identifier);
|
|
228
|
+
if (longContext !== undefined) {
|
|
229
|
+
return this.#consumeOptionValues(longContext, valueInlined);
|
|
261
230
|
}
|
|
262
|
-
|
|
263
|
-
new TypoText(
|
|
264
|
-
new TypoString(`Unexpected unknown option: `),
|
|
265
|
-
new TypoString(constant, typoStyleQuote),
|
|
266
|
-
),
|
|
267
|
-
);
|
|
231
|
+
this.#throwUnknownOptionError(identifier);
|
|
268
232
|
}
|
|
269
233
|
|
|
270
234
|
#tryConsumeOptionShort(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
rest: string,
|
|
235
|
+
shortContext: Context<ReaderOptionShortSpec>,
|
|
236
|
+
tokenRest: string,
|
|
274
237
|
): boolean {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.#consumeOptionValues(optionContext, constant, rest.slice(1));
|
|
238
|
+
if (tokenRest.startsWith("=")) {
|
|
239
|
+
this.#consumeOptionValues(shortContext, tokenRest.slice(1));
|
|
278
240
|
return true;
|
|
279
241
|
}
|
|
280
|
-
if (
|
|
281
|
-
this.#consumeOptionValues(
|
|
242
|
+
if (tokenRest.length === 0) {
|
|
243
|
+
this.#consumeOptionValues(shortContext, null);
|
|
282
244
|
return true;
|
|
283
245
|
}
|
|
284
|
-
if (
|
|
285
|
-
this.#consumeOptionValues(
|
|
246
|
+
if (shortContext.spec.restGuard(tokenRest)) {
|
|
247
|
+
this.#consumeOptionValues(shortContext, tokenRest);
|
|
286
248
|
return true;
|
|
287
249
|
}
|
|
288
|
-
this.#consumeOptionValues(
|
|
250
|
+
this.#consumeOptionValues(shortContext, null);
|
|
289
251
|
return false;
|
|
290
252
|
}
|
|
291
253
|
|
|
292
254
|
#consumeOptionValues(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
inlined: string | null,
|
|
255
|
+
context: Context<{ nextGuard: ReaderOptionNextGuard }>,
|
|
256
|
+
valueInlined: string | null,
|
|
296
257
|
) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
258
|
+
const value = { inlined: valueInlined, separated: new Array<string>() };
|
|
259
|
+
const { identifier, values, spec } = context;
|
|
260
|
+
values.push(value);
|
|
261
|
+
while (true) {
|
|
262
|
+
const nextToken = this.#tokens[this.#parsedIndex];
|
|
263
|
+
if (!spec.nextGuard(value, nextToken)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const token = this.#consumeToken();
|
|
267
|
+
if (this.#parsedDouble) {
|
|
268
|
+
throw new TypoError(
|
|
269
|
+
new TypoText(
|
|
270
|
+
new TypoString(identifier, typoStyleConstants),
|
|
271
|
+
new TypoString(`: Requires a value but got: `),
|
|
272
|
+
new TypoString(`"--"`, typoStyleQuote),
|
|
273
|
+
new TypoString(`.`),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
// TODO - should we allow consuming the EOF token ?
|
|
278
|
+
if (token === undefined) {
|
|
279
|
+
throw new TypoError(
|
|
280
|
+
new TypoText(
|
|
281
|
+
new TypoString(identifier, typoStyleConstants),
|
|
282
|
+
new TypoString(`: Requires a value, but got end of input.`), // TODO - hint at option value syntax ?
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
// TODO - is that weird, could a valid value start with dash ?
|
|
287
|
+
if (token.startsWith("-")) {
|
|
288
|
+
throw new TypoError(
|
|
289
|
+
new TypoText(
|
|
290
|
+
new TypoString(identifier, typoStyleConstants),
|
|
291
|
+
new TypoString(`: Requires a value, but got: `),
|
|
292
|
+
new TypoString(`"${token}"`, typoStyleQuote),
|
|
293
|
+
new TypoString(`.`),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
value.separated.push(token);
|
|
306
298
|
}
|
|
307
|
-
optionContext.results.push({ inlined, separated });
|
|
308
299
|
}
|
|
309
300
|
|
|
310
|
-
#
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
new TypoText(
|
|
315
|
-
new TypoString(constant, typoStyleConstants),
|
|
316
|
-
new TypoString(`: Requires a value, but got end of input`),
|
|
317
|
-
),
|
|
318
|
-
);
|
|
301
|
+
#throwUnknownOptionError(inputIdentifier: string): never {
|
|
302
|
+
const candidatesIdentifiers = [];
|
|
303
|
+
for (const identifier of this.#optionLongContextByIdentifier.keys()) {
|
|
304
|
+
candidatesIdentifiers.push(identifier);
|
|
319
305
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
new TypoText(
|
|
323
|
-
new TypoString(constant, typoStyleConstants),
|
|
324
|
-
new TypoString(`: Requires a value before `),
|
|
325
|
-
new TypoString(`"--"`, typoStyleQuote),
|
|
326
|
-
),
|
|
327
|
-
);
|
|
306
|
+
for (const identifier of this.#optionShortContextByIdentifier.keys()) {
|
|
307
|
+
candidatesIdentifiers.push(identifier);
|
|
328
308
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
309
|
+
const errorText = new TypoText();
|
|
310
|
+
errorText.push(new TypoString(`Unknown option: `));
|
|
311
|
+
errorText.push(new TypoString(`"${inputIdentifier}"`, typoStyleQuote));
|
|
312
|
+
if (candidatesIdentifiers.length === 0) {
|
|
313
|
+
errorText.push(new TypoString(`, no options are registered.`));
|
|
314
|
+
} else {
|
|
315
|
+
errorText.push(new TypoString(`.`));
|
|
316
|
+
suggestTextPushMessage(
|
|
317
|
+
errorText,
|
|
318
|
+
inputIdentifier,
|
|
319
|
+
candidatesIdentifiers.map((candidateIdentifier) => ({
|
|
320
|
+
reference: candidateIdentifier,
|
|
321
|
+
hint: new TypoString(candidateIdentifier, typoStyleConstants),
|
|
322
|
+
})),
|
|
337
323
|
);
|
|
338
324
|
}
|
|
339
|
-
|
|
325
|
+
throw new TypoError(errorText);
|
|
340
326
|
}
|
|
327
|
+
}
|
|
341
328
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
329
|
+
function isValidOptionKey(name: string): boolean {
|
|
330
|
+
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
345
331
|
}
|
|
346
332
|
|
|
347
|
-
type
|
|
348
|
-
|
|
349
|
-
|
|
333
|
+
type Context<Spec> = {
|
|
334
|
+
identifier: string;
|
|
335
|
+
spec: Spec;
|
|
336
|
+
values: Array<ReaderOptionValue>;
|
|
350
337
|
};
|
package/src/lib/Run.ts
CHANGED
|
@@ -75,9 +75,9 @@ export async function runAndExit<Context>(
|
|
|
75
75
|
if (colorSetup === "flag") {
|
|
76
76
|
const colorOption = optionSingleValue<"auto" | RunColorMode>({
|
|
77
77
|
long: "color",
|
|
78
|
-
type: typeChoice("color-mode", ["auto", "always", "never"
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
type: typeChoice("color-mode", ["auto", "always", "never"]),
|
|
79
|
+
fallbackValueIfAbsent: () => "auto",
|
|
80
|
+
impliedValueIfNotInlined: () => "always",
|
|
81
81
|
}).registerAndMakeDecoder(readerArgs);
|
|
82
82
|
preprocessors.push(() => {
|
|
83
83
|
try {
|
|
@@ -103,7 +103,7 @@ export async function runAndExit<Context>(
|
|
|
103
103
|
return 0;
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
-
if (options?.buildVersion) {
|
|
106
|
+
if (options?.buildVersion !== undefined) {
|
|
107
107
|
const versionOption = optionFlag({
|
|
108
108
|
long: "version",
|
|
109
109
|
}).registerAndMakeDecoder(readerArgs);
|
|
@@ -115,6 +115,8 @@ export async function runAndExit<Context>(
|
|
|
115
115
|
return 0;
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
+
// TODO - the lifecycle of this function should be improved
|
|
119
|
+
// TODO - how to pass the color information to the command logic ?
|
|
118
120
|
/*
|
|
119
121
|
// TODO - handle completions ?
|
|
120
122
|
readerArgs.registerFlag({
|
|
@@ -145,27 +147,36 @@ export async function runAndExit<Context>(
|
|
|
145
147
|
await commandInterpreter.executeWithContext(context);
|
|
146
148
|
return onExit(0);
|
|
147
149
|
} catch (executionError) {
|
|
148
|
-
handleError(options?.onError, executionError, typoSupport);
|
|
150
|
+
handleError(cliName, options?.onError, executionError, typoSupport);
|
|
149
151
|
return onExit(1);
|
|
150
152
|
}
|
|
151
153
|
} catch (parsingError) {
|
|
152
154
|
if (options?.usageOnError ?? true) {
|
|
153
155
|
console.error(computeUsageString(cliName, commandDecoder, typoSupport));
|
|
154
156
|
}
|
|
155
|
-
handleError(options?.onError, parsingError, typoSupport);
|
|
157
|
+
handleError(cliName, options?.onError, parsingError, typoSupport);
|
|
156
158
|
return onExit(1);
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
function handleError(
|
|
163
|
+
_cliName: string,
|
|
161
164
|
onError: ((error: unknown) => void) | undefined,
|
|
162
165
|
error: unknown,
|
|
163
166
|
typoSupport: TypoSupport,
|
|
164
167
|
) {
|
|
168
|
+
// TODO - should the cliName be part of the error message for logs ?
|
|
169
|
+
const finalError = error;
|
|
170
|
+
/*
|
|
171
|
+
const finalError = new TypoError(
|
|
172
|
+
new TypoText(new TypoString(cliName, typoStyleConstants)),
|
|
173
|
+
error,
|
|
174
|
+
);
|
|
175
|
+
*/
|
|
165
176
|
if (onError !== undefined) {
|
|
166
|
-
onError(
|
|
177
|
+
onError(finalError);
|
|
167
178
|
} else {
|
|
168
|
-
console.error(typoSupport.computeStyledErrorMessage(
|
|
179
|
+
console.error(typoSupport.computeStyledErrorMessage(finalError));
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TypoSegment, TypoString, TypoText } from "./Typo";
|
|
2
|
+
|
|
3
|
+
export function suggestTextPushMessage(
|
|
4
|
+
text: TypoText,
|
|
5
|
+
query: string,
|
|
6
|
+
candidates: Array<{ reference: string; hint: TypoSegment }>,
|
|
7
|
+
) {
|
|
8
|
+
const reasonableHints = suggestReasonablePayloads(
|
|
9
|
+
query,
|
|
10
|
+
candidates.map(({ reference, hint }) => ({ reference, payload: hint })),
|
|
11
|
+
);
|
|
12
|
+
if (reasonableHints.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
text.push(new TypoString(" Did you mean: "));
|
|
16
|
+
text.pushJoined(reasonableHints, new TypoString(", "), 3);
|
|
17
|
+
text.push(new TypoString(` ?`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function suggestReasonablePayloads<Payload>(
|
|
21
|
+
query: string,
|
|
22
|
+
candidates: Array<{ reference: string; payload: Payload }>,
|
|
23
|
+
): Array<Payload> {
|
|
24
|
+
if (candidates.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const sortedAlternatives = computeAndSortByDivergences(query, candidates);
|
|
28
|
+
const divergenceThreshold = sortedAlternatives[0]!.divergence + 0.25;
|
|
29
|
+
const acceptablePayloads = new Array<Payload>();
|
|
30
|
+
for (const { divergence, payload } of sortedAlternatives) {
|
|
31
|
+
if (divergence > divergenceThreshold) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
acceptablePayloads.push(payload);
|
|
35
|
+
}
|
|
36
|
+
return acceptablePayloads;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeAndSortByDivergences<Payload>(
|
|
40
|
+
query: string,
|
|
41
|
+
candidates: Array<{ reference: string; payload: Payload }>,
|
|
42
|
+
): Array<{ divergence: number; payload: Payload }> {
|
|
43
|
+
const queryNormalized = query.toLowerCase().slice(0, 100);
|
|
44
|
+
const scored = candidates.map(({ reference, payload }) => {
|
|
45
|
+
const referenceNormalized = reference.toLowerCase().slice(0, 100);
|
|
46
|
+
const divergence =
|
|
47
|
+
distanceDamerauLevenshtein(queryNormalized, referenceNormalized) /
|
|
48
|
+
Math.max(queryNormalized.length, referenceNormalized.length);
|
|
49
|
+
return { divergence, reference, payload };
|
|
50
|
+
});
|
|
51
|
+
return scored.sort((a, b) => a.divergence - b.divergence);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function distanceDamerauLevenshtein(a: string, b: string): number {
|
|
55
|
+
const m = a.length;
|
|
56
|
+
const n = b.length;
|
|
57
|
+
const dp = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0));
|
|
58
|
+
for (let i = 0; i <= m; i++) {
|
|
59
|
+
dp[i]![0] = i;
|
|
60
|
+
}
|
|
61
|
+
for (let j = 0; j <= n; j++) {
|
|
62
|
+
dp[0]![j] = j;
|
|
63
|
+
}
|
|
64
|
+
for (let i = 1; i <= m; i++) {
|
|
65
|
+
for (let j = 1; j <= n; j++) {
|
|
66
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
67
|
+
dp[i]![j] = Math.min(
|
|
68
|
+
dp[i - 1]![j]! + 1,
|
|
69
|
+
dp[i]![j - 1]! + 1,
|
|
70
|
+
dp[i - 1]![j - 1]! + cost,
|
|
71
|
+
);
|
|
72
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
73
|
+
dp[i]![j] = Math.min(dp[i]![j]!, dp[i - 2]![j - 2]! + cost);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return dp[m]![n]!;
|
|
78
|
+
}
|