cli-kiss 0.0.1
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/.github/workflows/ci.yml +21 -0
- package/.prettierrc +13 -0
- package/README.md +3 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/jest.config.ts +6 -0
- package/package.json +23 -0
- package/src/index.ts +7 -0
- package/src/lib/Argument.ts +93 -0
- package/src/lib/Command.ts +146 -0
- package/src/lib/Option.ts +140 -0
- package/src/lib/Processor.ts +87 -0
- package/src/lib/Reader.ts +285 -0
- package/src/lib/Run.ts +58 -0
- package/src/lib/Type.ts +59 -0
- package/src/lib/Usage.ts +104 -0
- package/tests/unit.Reader.aliases.ts +46 -0
- package/tests/unit.Reader.commons.ts +124 -0
- package/tests/unit.Reader.shortBig.ts +86 -0
- package/tests/unit.command.run.ts +139 -0
- package/tests/unit.command.usage.ts +153 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ReaderTokenizer } from "./Reader";
|
|
2
|
+
import { Type } from "./Type";
|
|
3
|
+
|
|
4
|
+
export type Option<Value> = {
|
|
5
|
+
generateUsage(): OptionUsage;
|
|
6
|
+
prepareConsumer(readerTokenizer: ReaderTokenizer): OptionConsumer<Value>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type OptionUsage = {
|
|
10
|
+
description: string | undefined;
|
|
11
|
+
long: Lowercase<string>; // TODO - better type for long option names ?
|
|
12
|
+
short: string | undefined;
|
|
13
|
+
label: Uppercase<string> | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OptionConsumer<Value> = () => Value;
|
|
17
|
+
|
|
18
|
+
export function optionFlag(definition: {
|
|
19
|
+
description?: string;
|
|
20
|
+
long: Lowercase<string>;
|
|
21
|
+
short?: string;
|
|
22
|
+
aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
|
|
23
|
+
default?: () => boolean;
|
|
24
|
+
}): Option<boolean> {
|
|
25
|
+
return {
|
|
26
|
+
generateUsage() {
|
|
27
|
+
return {
|
|
28
|
+
description: definition.description,
|
|
29
|
+
long: definition.long,
|
|
30
|
+
short: definition.short,
|
|
31
|
+
label: undefined,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
prepareConsumer(readerTokenizer: ReaderTokenizer) {
|
|
35
|
+
const key = definition.long;
|
|
36
|
+
const longs = [definition.long];
|
|
37
|
+
if (definition.aliases?.longs) {
|
|
38
|
+
longs.push(...definition.aliases?.longs);
|
|
39
|
+
}
|
|
40
|
+
const shorts = definition.short ? [definition.short] : [];
|
|
41
|
+
if (definition.aliases?.shorts) {
|
|
42
|
+
shorts.push(...definition.aliases?.shorts);
|
|
43
|
+
}
|
|
44
|
+
readerTokenizer.registerFlag({ key, longs, shorts });
|
|
45
|
+
return () => {
|
|
46
|
+
const value = readerTokenizer.consumeFlag(key);
|
|
47
|
+
if (value === undefined) {
|
|
48
|
+
return definition.default ? definition.default() : false;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO - option with comma-separated values, e.g. --names=alice,bob,charlie
|
|
57
|
+
|
|
58
|
+
export function optionRepeatable<Value>(definition: {
|
|
59
|
+
description?: string;
|
|
60
|
+
type: Type<Value>;
|
|
61
|
+
long: Lowercase<string>;
|
|
62
|
+
short?: string;
|
|
63
|
+
aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
|
|
64
|
+
label?: Uppercase<string>;
|
|
65
|
+
}): Option<Array<Value>> {
|
|
66
|
+
return {
|
|
67
|
+
generateUsage() {
|
|
68
|
+
return {
|
|
69
|
+
description: definition.description,
|
|
70
|
+
long: definition.long,
|
|
71
|
+
short: definition.short,
|
|
72
|
+
label:
|
|
73
|
+
`<${definition.label ?? definition.type.label}>` as Uppercase<string>,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
prepareConsumer(readerTokenizer: ReaderTokenizer) {
|
|
77
|
+
const key = definition.long;
|
|
78
|
+
const longs = definition.long ? [definition.long] : [];
|
|
79
|
+
if (definition.aliases?.longs) {
|
|
80
|
+
longs.push(...definition.aliases?.longs);
|
|
81
|
+
}
|
|
82
|
+
const shorts = definition.short ? [definition.short] : [];
|
|
83
|
+
if (definition.aliases?.shorts) {
|
|
84
|
+
shorts.push(...definition.aliases?.shorts);
|
|
85
|
+
}
|
|
86
|
+
readerTokenizer.registerOption({ key, longs, shorts });
|
|
87
|
+
return () => {
|
|
88
|
+
return readerTokenizer.consumeOption(key).map(definition.type.decoder);
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function optionSingleValue<Value>(definition: {
|
|
95
|
+
description?: string;
|
|
96
|
+
type: Type<Value>;
|
|
97
|
+
long: Lowercase<string>;
|
|
98
|
+
short?: string;
|
|
99
|
+
aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
|
|
100
|
+
label?: Uppercase<string>;
|
|
101
|
+
default: () => Value;
|
|
102
|
+
}): Option<Value> {
|
|
103
|
+
return {
|
|
104
|
+
generateUsage() {
|
|
105
|
+
return {
|
|
106
|
+
description: definition.description,
|
|
107
|
+
long: definition.long,
|
|
108
|
+
short: definition.short,
|
|
109
|
+
label:
|
|
110
|
+
`<${definition.label ?? definition.type.label}>` as Uppercase<string>,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
prepareConsumer(readerTokenizer: ReaderTokenizer) {
|
|
114
|
+
const key = definition.long;
|
|
115
|
+
const longs = [definition.long];
|
|
116
|
+
if (definition.aliases?.longs) {
|
|
117
|
+
longs.push(...definition.aliases?.longs);
|
|
118
|
+
}
|
|
119
|
+
const shorts = definition.short ? [definition.short] : [];
|
|
120
|
+
if (definition.aliases?.shorts) {
|
|
121
|
+
shorts.push(...definition.aliases?.shorts);
|
|
122
|
+
}
|
|
123
|
+
readerTokenizer.registerOption({ key, longs, shorts });
|
|
124
|
+
return () => {
|
|
125
|
+
// TODO - error handling
|
|
126
|
+
const values = readerTokenizer.consumeOption(definition.long);
|
|
127
|
+
if (values.length > 1) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Multiple values provided for option: ${definition.long}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const firstValue = values[0];
|
|
133
|
+
if (firstValue === undefined) {
|
|
134
|
+
return definition.default();
|
|
135
|
+
}
|
|
136
|
+
return definition.type.decoder(firstValue);
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Argument, ArgumentUsage } from "./Argument";
|
|
2
|
+
import { Option, OptionUsage } from "./Option";
|
|
3
|
+
import { ReaderTokenizer } from "./Reader";
|
|
4
|
+
|
|
5
|
+
export type Processor<Context, Result> = {
|
|
6
|
+
computeUsage(): ProcessorUsage;
|
|
7
|
+
prepareResolver(
|
|
8
|
+
readerTokenizer: ReaderTokenizer,
|
|
9
|
+
): ProcessorResolver<Context, Result>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ProcessorResolver<Context, Result> = () => ProcessorRunner<
|
|
13
|
+
Context,
|
|
14
|
+
Result
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export type ProcessorRunner<Context, Result> = {
|
|
18
|
+
execute(context: Context): Promise<Result>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ProcessorUsage = {
|
|
22
|
+
options: Array<OptionUsage>;
|
|
23
|
+
arguments: Array<ArgumentUsage>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function processor<
|
|
27
|
+
Context,
|
|
28
|
+
Result,
|
|
29
|
+
Options extends { [option: string]: Option<any> },
|
|
30
|
+
const Arguments extends Array<Argument<any>>,
|
|
31
|
+
>(
|
|
32
|
+
inputs: { options: Options; arguments: Arguments },
|
|
33
|
+
handler: (
|
|
34
|
+
context: Context,
|
|
35
|
+
inputs: {
|
|
36
|
+
options: {
|
|
37
|
+
[K in keyof Options]: ReturnType<
|
|
38
|
+
ReturnType<Options[K]["prepareConsumer"]>
|
|
39
|
+
>;
|
|
40
|
+
};
|
|
41
|
+
arguments: {
|
|
42
|
+
[K in keyof Arguments]: ReturnType<Arguments[K]["consumeValue"]>;
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
) => Promise<Result>,
|
|
46
|
+
): Processor<Context, Result> {
|
|
47
|
+
return {
|
|
48
|
+
computeUsage() {
|
|
49
|
+
const optionsUsage = new Array<OptionUsage>();
|
|
50
|
+
for (const optionKey in inputs.options) {
|
|
51
|
+
const optionInput = inputs.options[optionKey]!;
|
|
52
|
+
optionsUsage.push(optionInput.generateUsage());
|
|
53
|
+
}
|
|
54
|
+
const argumentsUsage = new Array<ArgumentUsage>();
|
|
55
|
+
for (const argumentInput of inputs.arguments) {
|
|
56
|
+
argumentsUsage.push(argumentInput.generateUsage());
|
|
57
|
+
}
|
|
58
|
+
return { options: optionsUsage, arguments: argumentsUsage };
|
|
59
|
+
},
|
|
60
|
+
prepareResolver(readerTokenizer: ReaderTokenizer) {
|
|
61
|
+
const optionsConsumers: any = {};
|
|
62
|
+
for (const optionKey in inputs.options) {
|
|
63
|
+
const optionInput = inputs.options[optionKey]!;
|
|
64
|
+
optionsConsumers[optionKey] =
|
|
65
|
+
optionInput.prepareConsumer(readerTokenizer);
|
|
66
|
+
}
|
|
67
|
+
const argumentsValues: any = [];
|
|
68
|
+
for (const argumentInput of inputs.arguments) {
|
|
69
|
+
argumentsValues.push(argumentInput.consumeValue(readerTokenizer));
|
|
70
|
+
}
|
|
71
|
+
return () => {
|
|
72
|
+
const optionsValues: any = {};
|
|
73
|
+
for (const optionKey in optionsConsumers) {
|
|
74
|
+
optionsValues[optionKey] = optionsConsumers[optionKey]!();
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
async execute(context: Context) {
|
|
78
|
+
return await handler(context, {
|
|
79
|
+
options: optionsValues,
|
|
80
|
+
arguments: argumentsValues,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
export type ReaderPositionals = {
|
|
2
|
+
consumePositional(): string | undefined;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export class ReaderTokenizer {
|
|
6
|
+
#parsedArgv: Array<string>;
|
|
7
|
+
#parsedIndex: number;
|
|
8
|
+
#parsedDouble: boolean;
|
|
9
|
+
|
|
10
|
+
#flagKeyByShort: Map<string, string>;
|
|
11
|
+
#flagKeyByLong: Map<string, string>;
|
|
12
|
+
#flagInfoByKey: Map<string, {}>;
|
|
13
|
+
#flagResultByKey: Map<string, boolean | null>;
|
|
14
|
+
|
|
15
|
+
#optionKeyByShort: Map<string, string>;
|
|
16
|
+
#optionKeyByLong: Map<string, string>;
|
|
17
|
+
#optionInfoByKey: Map<string, {}>; // TODO - what dis for
|
|
18
|
+
#optionResultByKey: Map<string, Array<string> | null>;
|
|
19
|
+
|
|
20
|
+
constructor(argv: Array<string>) {
|
|
21
|
+
this.#parsedArgv = argv;
|
|
22
|
+
this.#parsedIndex = 0;
|
|
23
|
+
this.#parsedDouble = false;
|
|
24
|
+
|
|
25
|
+
// TODO - this seems like a good candidate for abstraction
|
|
26
|
+
this.#flagKeyByShort = new Map();
|
|
27
|
+
this.#flagKeyByLong = new Map();
|
|
28
|
+
this.#flagInfoByKey = new Map();
|
|
29
|
+
this.#flagResultByKey = new Map();
|
|
30
|
+
|
|
31
|
+
this.#optionKeyByShort = new Map();
|
|
32
|
+
this.#optionKeyByLong = new Map();
|
|
33
|
+
this.#optionInfoByKey = new Map();
|
|
34
|
+
this.#optionResultByKey = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
registerFlag(definition: {
|
|
38
|
+
key: string;
|
|
39
|
+
shorts: Array<string>;
|
|
40
|
+
longs: Array<string>;
|
|
41
|
+
}) {
|
|
42
|
+
this.#ensureUniqueKey(definition.key);
|
|
43
|
+
this.#flagInfoByKey.set(definition.key, {});
|
|
44
|
+
for (const short of definition.shorts) {
|
|
45
|
+
this.#ensureUniqueName(short);
|
|
46
|
+
this.#flagKeyByShort.set(short, definition.key);
|
|
47
|
+
}
|
|
48
|
+
for (const long of definition.longs) {
|
|
49
|
+
this.#ensureUniqueName(long);
|
|
50
|
+
this.#flagKeyByLong.set(long, definition.key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerOption(definition: {
|
|
55
|
+
key: string;
|
|
56
|
+
shorts: Array<string>;
|
|
57
|
+
longs: Array<string>;
|
|
58
|
+
}) {
|
|
59
|
+
this.#ensureUniqueKey(definition.key);
|
|
60
|
+
this.#optionInfoByKey.set(definition.key, {});
|
|
61
|
+
for (const short of definition.shorts) {
|
|
62
|
+
this.#ensureUniqueName(short);
|
|
63
|
+
this.#optionKeyByShort.set(short, definition.key);
|
|
64
|
+
}
|
|
65
|
+
for (const long of definition.longs) {
|
|
66
|
+
this.#ensureUniqueName(long);
|
|
67
|
+
this.#optionKeyByLong.set(long, definition.key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
consumeFlag(key: string): boolean | undefined {
|
|
72
|
+
const flagInfo = this.#flagInfoByKey.get(key);
|
|
73
|
+
if (flagInfo === undefined) {
|
|
74
|
+
throw new Error(`Option flag not registered: ${key}`);
|
|
75
|
+
}
|
|
76
|
+
const result = this.#flagResultByKey.get(key);
|
|
77
|
+
if (result === undefined) {
|
|
78
|
+
this.#flagResultByKey.set(key, null);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (result === null) {
|
|
82
|
+
throw new Error(`Option flag already consumed: ${key}`);
|
|
83
|
+
}
|
|
84
|
+
this.#flagResultByKey.set(key, null);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
consumeOption(key: string): Array<string> {
|
|
89
|
+
const optionInfo = this.#optionInfoByKey.get(key);
|
|
90
|
+
if (optionInfo === undefined) {
|
|
91
|
+
throw new Error(`Option values not registered: ${key}`);
|
|
92
|
+
}
|
|
93
|
+
const result = this.#optionResultByKey.get(key);
|
|
94
|
+
if (result === undefined) {
|
|
95
|
+
this.#optionResultByKey.set(key, null);
|
|
96
|
+
return new Array<string>();
|
|
97
|
+
}
|
|
98
|
+
if (result === null) {
|
|
99
|
+
throw new Error(`Option values already consumed: ${key}`);
|
|
100
|
+
}
|
|
101
|
+
this.#optionResultByKey.set(key, null);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
consumePositional(): string | undefined {
|
|
106
|
+
while (true) {
|
|
107
|
+
const arg = this.#consumeArg();
|
|
108
|
+
if (arg === null) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const positional = this.#parseAsPositional(arg);
|
|
112
|
+
if (positional !== null) {
|
|
113
|
+
return positional;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#consumeArg(): string | null {
|
|
119
|
+
const arg = this.#parsedArgv[this.#parsedIndex];
|
|
120
|
+
if (arg === undefined) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
this.#parsedIndex++;
|
|
124
|
+
if (!this.#parsedDouble) {
|
|
125
|
+
if (arg === "--") {
|
|
126
|
+
this.#parsedDouble = true;
|
|
127
|
+
return this.#consumeArg();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return arg;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#consumeOptionValue(name: string) {
|
|
134
|
+
const arg = this.#consumeArg();
|
|
135
|
+
if (arg === null) {
|
|
136
|
+
throw new Error(`Option ${name} requires a value`);
|
|
137
|
+
}
|
|
138
|
+
if (this.#parsedDouble) {
|
|
139
|
+
throw new Error(`Option ${name} requires a value before --`);
|
|
140
|
+
}
|
|
141
|
+
// TODO - is that weird, could a valid value start with dash ?
|
|
142
|
+
if (arg.startsWith("-")) {
|
|
143
|
+
throw new Error(`Option ${name} requires a value, got: ${arg}`);
|
|
144
|
+
}
|
|
145
|
+
return arg;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#parseAsPositional(arg: string): string | null {
|
|
149
|
+
if (this.#parsedDouble) {
|
|
150
|
+
return arg;
|
|
151
|
+
}
|
|
152
|
+
if (arg.startsWith("--")) {
|
|
153
|
+
const valueIndexStart = arg.indexOf("=");
|
|
154
|
+
if (valueIndexStart === -1) {
|
|
155
|
+
this.#consumeOptionLong(arg.slice(2), null);
|
|
156
|
+
} else {
|
|
157
|
+
this.#consumeOptionLong(
|
|
158
|
+
arg.slice(2, valueIndexStart),
|
|
159
|
+
arg.slice(valueIndexStart + 1),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (arg.startsWith("-")) {
|
|
165
|
+
let shortIndexStart = 1;
|
|
166
|
+
let shortIndexEnd = 2;
|
|
167
|
+
while (shortIndexEnd <= arg.length) {
|
|
168
|
+
const short = arg.slice(shortIndexStart, shortIndexEnd);
|
|
169
|
+
const rest = arg.slice(shortIndexEnd);
|
|
170
|
+
const result = this.#tryConsumeOptionShort(short, rest);
|
|
171
|
+
if (result === true) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (result === false) {
|
|
175
|
+
shortIndexStart = shortIndexEnd;
|
|
176
|
+
}
|
|
177
|
+
shortIndexEnd++;
|
|
178
|
+
}
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Unknown short flags or options: ${arg.slice(shortIndexStart)}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return arg;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#consumeOptionLong(long: string, direct: string | null): void {
|
|
187
|
+
const flagKey = this.#flagKeyByLong.get(long);
|
|
188
|
+
if (flagKey !== undefined) {
|
|
189
|
+
if (direct !== null) {
|
|
190
|
+
if (direct === "true") {
|
|
191
|
+
return this.#acknowledgeFlag(flagKey, true);
|
|
192
|
+
}
|
|
193
|
+
if (direct === "false") {
|
|
194
|
+
return this.#acknowledgeFlag(flagKey, false);
|
|
195
|
+
}
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Invalid parameter for long flag: ${flagKey}, value: ${direct}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return this.#acknowledgeFlag(flagKey, true);
|
|
201
|
+
}
|
|
202
|
+
const optionKey = this.#optionKeyByLong.get(long);
|
|
203
|
+
if (optionKey !== undefined) {
|
|
204
|
+
if (direct !== null) {
|
|
205
|
+
return this.#acknowledgeOption(optionKey, direct);
|
|
206
|
+
}
|
|
207
|
+
return this.#acknowledgeOption(optionKey, this.#consumeOptionValue(long));
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Unknown long flag or option: ${long}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#tryConsumeOptionShort(short: string, rest: string): boolean | null {
|
|
213
|
+
const flagKey = this.#flagKeyByShort.get(short);
|
|
214
|
+
if (flagKey !== undefined) {
|
|
215
|
+
if (rest.startsWith("=")) {
|
|
216
|
+
if (rest === "=true") {
|
|
217
|
+
this.#acknowledgeFlag(flagKey, true);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (rest === "=false") {
|
|
221
|
+
this.#acknowledgeFlag(flagKey, false);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Invalid parameter for short flag: ${short}, value: ${rest}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
this.#acknowledgeFlag(flagKey, true);
|
|
229
|
+
return rest === "";
|
|
230
|
+
}
|
|
231
|
+
const optionKey = this.#optionKeyByShort.get(short);
|
|
232
|
+
if (optionKey !== undefined) {
|
|
233
|
+
if (rest === "") {
|
|
234
|
+
this.#acknowledgeOption(optionKey, this.#consumeOptionValue(short));
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (rest.startsWith("=")) {
|
|
238
|
+
this.#acknowledgeOption(optionKey, rest.slice(1));
|
|
239
|
+
} else {
|
|
240
|
+
this.#acknowledgeOption(optionKey, rest);
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#acknowledgeFlag(key: string, value: boolean) {
|
|
248
|
+
if (this.#flagResultByKey.has(key)) {
|
|
249
|
+
throw new Error(`Flag already set: ${key}`);
|
|
250
|
+
}
|
|
251
|
+
this.#flagResultByKey.set(key, value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#acknowledgeOption(key: string, value: string) {
|
|
255
|
+
const values = this.#optionResultByKey.get(key) ?? new Array<string>();
|
|
256
|
+
values.push(value);
|
|
257
|
+
this.#optionResultByKey.set(key, values);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#ensureUniqueKey(key: string) {
|
|
261
|
+
if (this.#flagInfoByKey.has(key)) {
|
|
262
|
+
throw new Error(`Option already registered: ${key}`);
|
|
263
|
+
}
|
|
264
|
+
if (this.#optionInfoByKey.has(key)) {
|
|
265
|
+
throw new Error(`Option already registered: ${key}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#ensureUniqueName(nameShortOrLong: string) {
|
|
270
|
+
// TODO - overall better error handling
|
|
271
|
+
// TODO - short flag overlap might be annoying here
|
|
272
|
+
if (this.#flagKeyByShort.has(nameShortOrLong)) {
|
|
273
|
+
throw new Error(`Option already registered: ${nameShortOrLong}`);
|
|
274
|
+
}
|
|
275
|
+
if (this.#flagKeyByLong.has(nameShortOrLong)) {
|
|
276
|
+
throw new Error(`Option already registered: ${nameShortOrLong}`);
|
|
277
|
+
}
|
|
278
|
+
if (this.#optionKeyByShort.has(nameShortOrLong)) {
|
|
279
|
+
throw new Error(`Option already registered: ${nameShortOrLong}`);
|
|
280
|
+
}
|
|
281
|
+
if (this.#optionKeyByLong.has(nameShortOrLong)) {
|
|
282
|
+
throw new Error(`Option already registered: ${nameShortOrLong}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/lib/Run.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from "./Command";
|
|
2
|
+
import { ReaderTokenizer } from "./Reader";
|
|
3
|
+
import { usageFormatter } from "./Usage";
|
|
4
|
+
|
|
5
|
+
export async function runWithArgv<Context, Result>(
|
|
6
|
+
argv: string[],
|
|
7
|
+
context: Context,
|
|
8
|
+
command: Command<Context, Result>,
|
|
9
|
+
cliInfo?: { name?: string; version?: string; helpOnError?: boolean },
|
|
10
|
+
): Promise<Result> {
|
|
11
|
+
const cliName = cliInfo?.name ?? argv[1]!;
|
|
12
|
+
const readerTokenizer = new ReaderTokenizer(argv.slice(2));
|
|
13
|
+
readerTokenizer.registerFlag({
|
|
14
|
+
key: "help",
|
|
15
|
+
shorts: [],
|
|
16
|
+
longs: ["help"],
|
|
17
|
+
});
|
|
18
|
+
if (cliInfo?.version) {
|
|
19
|
+
readerTokenizer.registerFlag({
|
|
20
|
+
key: "version",
|
|
21
|
+
shorts: [],
|
|
22
|
+
longs: ["version"],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/*
|
|
26
|
+
// TODO - handle completions ?
|
|
27
|
+
readerTokenizer.registerFlag({
|
|
28
|
+
key: "completion",
|
|
29
|
+
shorts: [],
|
|
30
|
+
longs: ["completion"],
|
|
31
|
+
});
|
|
32
|
+
*/
|
|
33
|
+
try {
|
|
34
|
+
const commandRunner = command.prepareRunner(readerTokenizer);
|
|
35
|
+
if (cliInfo?.version) {
|
|
36
|
+
if (readerTokenizer.consumeFlag("version")) {
|
|
37
|
+
console.log(cliName, cliInfo.version);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (readerTokenizer.consumeFlag("help")) {
|
|
42
|
+
console.log(usageFormatter(cliName, commandRunner.computeUsage()));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await commandRunner.execute(context);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (cliInfo?.helpOnError ?? true) {
|
|
49
|
+
console.log(usageFormatter(cliName, commandRunner.computeUsage()));
|
|
50
|
+
}
|
|
51
|
+
console.error(error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/lib/Type.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type Type<Value> = {
|
|
2
|
+
label: Uppercase<string>; // TODO - is there a better way to enforce uppercase labels?
|
|
3
|
+
decoder(value: string): Value;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const typeBoolean: Type<boolean> = {
|
|
7
|
+
label: "BOOLEAN",
|
|
8
|
+
decoder(value: string) {
|
|
9
|
+
if (value === "true") {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (value === "false") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Invalid boolean value: ${value}`);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const typeDate: Type<Date> = {
|
|
20
|
+
label: "DATE",
|
|
21
|
+
decoder(value: string) {
|
|
22
|
+
const timestamp = Date.parse(value);
|
|
23
|
+
if (isNaN(timestamp)) {
|
|
24
|
+
throw new Error(`Invalid date value: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
return new Date(timestamp);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const typeString: Type<string> = {
|
|
31
|
+
label: "STRING",
|
|
32
|
+
decoder(value: string) {
|
|
33
|
+
return value;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const typeNumber: Type<number> = {
|
|
38
|
+
label: "NUMBER",
|
|
39
|
+
decoder(value: string) {
|
|
40
|
+
return Number(value);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const typeBigInt: Type<bigint> = {
|
|
45
|
+
label: "BIGINT",
|
|
46
|
+
decoder(value: string) {
|
|
47
|
+
return BigInt(value);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function typeCommaArray(elementType: Type<any>): Type<Array<any>> {
|
|
52
|
+
return {
|
|
53
|
+
label:
|
|
54
|
+
`${elementType.label}[${elementType.label},...]` as Uppercase<string>,
|
|
55
|
+
decoder(value: string) {
|
|
56
|
+
return value.split(",").map(elementType.decoder);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|