@flowscripter/dynamic-cli-framework 1.0.0

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.
Files changed (133) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +1475 -0
  7. package/bun.lock +63 -0
  8. package/index.ts +89 -0
  9. package/package.json +23 -0
  10. package/src/api/CLI.ts +17 -0
  11. package/src/api/CLIConfig.ts +19 -0
  12. package/src/api/Context.ts +25 -0
  13. package/src/api/RunResult.ts +142 -0
  14. package/src/api/argument/Argument.ts +36 -0
  15. package/src/api/argument/ArgumentValueTypes.ts +87 -0
  16. package/src/api/argument/ComplexOption.ts +31 -0
  17. package/src/api/argument/GlobalCommandArgument.ts +17 -0
  18. package/src/api/argument/Option.ts +32 -0
  19. package/src/api/argument/Positional.ts +27 -0
  20. package/src/api/argument/SubCommandArgument.ts +20 -0
  21. package/src/api/command/Command.ts +22 -0
  22. package/src/api/command/GlobalCommand.ts +33 -0
  23. package/src/api/command/GlobalModifierCommand.ts +12 -0
  24. package/src/api/command/GroupCommand.ts +20 -0
  25. package/src/api/command/SubCommand.ts +41 -0
  26. package/src/api/command/UsageExample.ts +21 -0
  27. package/src/api/service/ServiceProvider.ts +52 -0
  28. package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
  29. package/src/api/service/core/KeyValueService.ts +28 -0
  30. package/src/api/service/core/PrinterService.ts +266 -0
  31. package/src/api/service/core/ShutdownService.ts +12 -0
  32. package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
  33. package/src/cli/BaseCLI.ts +312 -0
  34. package/src/cli/DefaultRuntimeCLI.ts +54 -0
  35. package/src/command/MultiCommandCliHelpCommand.ts +394 -0
  36. package/src/command/SingleCommandCliHelpCommand.ts +183 -0
  37. package/src/command/UsageCommand.ts +35 -0
  38. package/src/command/VersionCommand.ts +22 -0
  39. package/src/launcher.ts +127 -0
  40. package/src/runtime/DefaultContext.ts +32 -0
  41. package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
  42. package/src/runtime/command/CommandTypeGuards.ts +30 -0
  43. package/src/runtime/command/CommandValidator.ts +549 -0
  44. package/src/runtime/parser.ts +219 -0
  45. package/src/runtime/registry/CommandRegistry.ts +121 -0
  46. package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
  47. package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
  48. package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
  49. package/src/runtime/runner.ts +697 -0
  50. package/src/runtime/scanner.ts +417 -0
  51. package/src/runtime/values/ValuePopulationResult.ts +41 -0
  52. package/src/runtime/values/argumentValueMerge.ts +201 -0
  53. package/src/runtime/values/argumentValueValidation.ts +610 -0
  54. package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
  55. package/src/runtime/values/subCommandValuePopulation.ts +680 -0
  56. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
  57. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
  58. package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
  59. package/src/service/banner/BannerServiceProvider.ts +99 -0
  60. package/src/service/banner/command/NoBannerCommand.ts +42 -0
  61. package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
  62. package/src/service/configuration/DefaultKeyValueService.ts +60 -0
  63. package/src/service/configuration/command/ConfigCommand.ts +43 -0
  64. package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
  65. package/src/service/printer/DefaultPrinterService.ts +256 -0
  66. package/src/service/printer/PrinterServiceProvider.ts +61 -0
  67. package/src/service/printer/command/DarkModeCommand.ts +43 -0
  68. package/src/service/printer/command/LogLevelCommand.ts +68 -0
  69. package/src/service/printer/command/NoColorCommand.ts +43 -0
  70. package/src/service/printer/terminal/Ansi.ts +10 -0
  71. package/src/service/printer/terminal/Color.ts +14 -0
  72. package/src/service/printer/terminal/Progress.ts +373 -0
  73. package/src/service/printer/terminal/Spinner.ts +100 -0
  74. package/src/service/printer/terminal/Styler.ts +5 -0
  75. package/src/service/printer/terminal/Terminal.ts +8 -0
  76. package/src/service/printer/terminal/Theme.ts +34 -0
  77. package/src/service/printer/terminal/TtyStyler.ts +39 -0
  78. package/src/service/printer/terminal/TtyTerminal.ts +65 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +9 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
  81. package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
  83. package/src/util/configHelper.ts +95 -0
  84. package/src/util/envVarHelper.ts +338 -0
  85. package/src/util/helpHelper.ts +751 -0
  86. package/src/util/logger.ts +84 -0
  87. package/src/util/runnerHelper.ts +112 -0
  88. package/tests/cli/BaseCLI.test.ts +344 -0
  89. package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
  90. package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
  91. package/tests/command/UsageCommand.test.ts +53 -0
  92. package/tests/command/VersionCommand.test.ts +36 -0
  93. package/tests/fixtures/CLIConfig.ts +9 -0
  94. package/tests/fixtures/Command.ts +229 -0
  95. package/tests/fixtures/CommandRegistry.ts +8 -0
  96. package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
  97. package/tests/fixtures/Context.ts +33 -0
  98. package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
  99. package/tests/fixtures/StreamString.ts +37 -0
  100. package/tests/fixtures/util.ts +41 -0
  101. package/tests/runtime/DefaultContext.test.ts +20 -0
  102. package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
  103. package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
  104. package/tests/runtime/command/CommandValidator.test.ts +816 -0
  105. package/tests/runtime/parser.test.ts +766 -0
  106. package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
  107. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
  108. package/tests/runtime/runner.test.ts +1298 -0
  109. package/tests/runtime/scanner.test.ts +1113 -0
  110. package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
  111. package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
  112. package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
  113. package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
  114. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
  115. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
  116. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
  117. package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
  118. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
  119. package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
  120. package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
  121. package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
  122. package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
  123. package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
  124. package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
  125. package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
  126. package/tests/service/printer/terminal/Progress.test.ts +40 -0
  127. package/tests/service/printer/terminal/Spinner.test.ts +57 -0
  128. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
  129. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
  130. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
  131. package/tests/util/configHelper.test.ts +275 -0
  132. package/tests/util/envVarHelper.test.ts +318 -0
  133. package/tests/util/logger.test.ts +10 -0
@@ -0,0 +1,680 @@
1
+ import type Option from "../../api/argument/Option.ts";
2
+ import type SubCommand from "../../api/command/SubCommand.ts";
3
+ import {
4
+ type ArgumentSingleValueType,
5
+ ArgumentValueTypeName,
6
+ type PopulatedArgumentSingleValueType,
7
+ type PopulatedArgumentValues,
8
+ type PopulatedArgumentValueType,
9
+ } from "../../api/argument/ArgumentValueTypes.ts";
10
+ import getLogger from "../../util/logger.ts";
11
+ import argumentValueMerge from "./argumentValueMerge.ts";
12
+ import type { SubCommandValuePopulationResult } from "./ValuePopulationResult.ts";
13
+ import type ComplexOption from "../../api/argument/ComplexOption.ts";
14
+ import {
15
+ MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH,
16
+ } from "../../api/argument/ComplexOption.ts";
17
+ import { isComplexOption } from "../argument/ArgumentTypeGuards.ts";
18
+ import {
19
+ type InvalidArgument,
20
+ InvalidArgumentReason,
21
+ } from "../../api/RunResult.ts";
22
+ import { MAXIMUM_ARGUMENT_ARRAY_SIZE } from "../../api/argument/SubCommandArgument.ts";
23
+
24
+ const logger = getLogger("subCommandValuePopulation");
25
+
26
+ /**
27
+ * Parse states for parsing loop logic
28
+ */
29
+ enum ParseState {
30
+ /**
31
+ * Initial state: expecting either an {@link Option} argument (`--x`, `-x`, `--x=y`, `-x=y`) or {@link Positional} value next.
32
+ */
33
+ EMPTY = 0,
34
+
35
+ /**
36
+ * Error while parsing.
37
+ */
38
+ ERROR = 1,
39
+
40
+ /**
41
+ * The argument was not used.
42
+ */
43
+ UNUSED = 2,
44
+
45
+ /**
46
+ * Found an {@link Option} name, expecting an {@link Option} value next.
47
+ */
48
+ OPTION_VALUE_EXPECTED = 3,
49
+ }
50
+
51
+ /**
52
+ * Context container for the state of the parsing loop.
53
+ */
54
+ class ParseContext {
55
+ /**
56
+ * The {@link SubCommand} for which args are being parsed.
57
+ */
58
+ subCommand: SubCommand;
59
+
60
+ /**
61
+ * The current populated values for the sub-command.
62
+ */
63
+ populatedArgumentValues: PopulatedArgumentValues;
64
+
65
+ /**
66
+ * Current parse state.
67
+ */
68
+ state: ParseState;
69
+
70
+ /**
71
+ * If {@link state} = {@link ParseState.ERROR} this will be populated with the details.
72
+ */
73
+ invalidArgument?: InvalidArgument;
74
+
75
+ /**
76
+ * An array of either option names or indices used to navigate through the {@link populatedArgumentValues} when
77
+ * setting a value for the {@link currentOption}.
78
+ */
79
+ currentOptionPath?: Array<string | number>;
80
+
81
+ /**
82
+ * An optional reference to the current {@link Option} being addressed when {@link state} = {@link ParseState.OPTION_VALUE_EXPECTED}.
83
+ */
84
+ currentOption?: Option;
85
+
86
+ /**
87
+ * Current index of {@link Positional} defined for the current {@link SubCommand} being passed
88
+ * which will be populated with a positional value if found.
89
+ */
90
+ currentPositionalIndex;
91
+
92
+ /**
93
+ * Use to store lazily created map of option paths e.g. --alpha.b.g and -a.b.gamma for {@link ComplexOption} or {@link Option} instances.
94
+ */
95
+ optionLookupMapsByPath: Map<string, Map<string, Option | ComplexOption>>;
96
+
97
+ /**
98
+ * Use to store map of root {@link ComplexOption} or {@link Option} instances by name.
99
+ */
100
+ rootOptionsByName: Map<string, Option | ComplexOption>;
101
+
102
+ /**
103
+ * Use to store map of root {@link ComplexOption} or {@link Option} instances by short alias.
104
+ */
105
+ rootOptionsByAlias: Map<string, Option | ComplexOption>;
106
+
107
+ /**
108
+ * Create a new instance with {@link state} = {@link ParseState.EMPTY} for the specified {@link SubCommand}.
109
+ *
110
+ * @param subCommand the {@link SubCommand} for which args are being parsed.
111
+ */
112
+ constructor(subCommand: SubCommand) {
113
+ this.subCommand = subCommand;
114
+ this.state = ParseState.EMPTY;
115
+ this.populatedArgumentValues = {};
116
+ this.currentPositionalIndex = 0;
117
+ this.optionLookupMapsByPath = new Map();
118
+ this.rootOptionsByName = new Map();
119
+ this.rootOptionsByAlias = new Map();
120
+ subCommand.options.forEach((option) => {
121
+ this.rootOptionsByName.set(option.name, option);
122
+ if (option.shortAlias) {
123
+ this.rootOptionsByAlias.set(option.shortAlias, option);
124
+ }
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Attempt to parse the {@link Option} path specified by the provided option argument.
130
+ *
131
+ * NOTE: the provided path could:
132
+ *
133
+ * * be either a reference to a simple option or a property on a nested complex option: `foo` or `alpha.beta.gamma.delta`
134
+ * * include either the option names or short aliases may be specified: `alpha.b.gamma.d`
135
+ * * include array indices e.g. `alpha[0].beta.gamma[1].delta`
136
+ *
137
+ * @param optionPath the option path argument to parse.
138
+ *
139
+ * @return `true` if successful, which means either:
140
+ * 1. the {@link state} will be set to {@link ParseState.OPTION_VALUE_EXPECTED} and the {@link currentOptionPath} and {@link currentOption} will be populated OR
141
+ * 2. the option path was not recognised as an option for the current sub-command.
142
+ * If `false` the {@link state} will be set to
143
+ * {@link ParseState.ERROR} and {@link invalidArgument} will be populated. This will be the case if the specified option path was initially
144
+ * found to be referring to a complex property for the current sub-command but parsing the entire path failed either due to an invalid expression
145
+ * or an unknown property.
146
+ */
147
+ #parseOptionPath(optionPath: string): boolean {
148
+ logger.debug("Parsing option path: %s", optionPath);
149
+ this.currentOptionPath = undefined;
150
+ this.currentOption = undefined;
151
+
152
+ // split up the path into elements
153
+ const complexPathElements = optionPath.split(
154
+ ".",
155
+ MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH + 1,
156
+ );
157
+
158
+ // check nesting depth
159
+ if (complexPathElements.length > MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH) {
160
+ this.invalidArgument = {
161
+ name: optionPath,
162
+ reason: InvalidArgumentReason.NESTING_DEPTH_EXCEEDED,
163
+ };
164
+ this.state = ParseState.ERROR;
165
+ return false;
166
+ }
167
+
168
+ const complexAndArrayPathElements: Array<string | number> = [];
169
+ let option: Option | ComplexOption | undefined = undefined;
170
+ let currentValue:
171
+ | PopulatedArgumentValues
172
+ | Array<PopulatedArgumentValues | PopulatedArgumentValueType>
173
+ | PopulatedArgumentValueType
174
+ | undefined = this.populatedArgumentValues;
175
+
176
+ for (let i = 0; i < complexPathElements.length; i++) {
177
+ let arrayIndexString: string | undefined;
178
+ let complexPathElement = complexPathElements[i];
179
+
180
+ // look for array index
181
+ if (complexPathElement.endsWith("]")) {
182
+ [complexPathElement, arrayIndexString] = complexPathElement.slice(
183
+ 0,
184
+ complexPathElement.length - 1,
185
+ ).split("[");
186
+ }
187
+
188
+ let lookupPath = this.subCommand.name;
189
+
190
+ // root option navigation
191
+ if (i === 0) {
192
+ if (complexPathElement.startsWith("--")) {
193
+ complexPathElement = complexPathElement.slice(2);
194
+ option = this.rootOptionsByName.get(complexPathElement);
195
+ } else {
196
+ complexPathElement = complexPathElement.slice(1);
197
+ // ignore if illegal syntax of `-more_than_one_letter`
198
+ if (complexPathElement.length > 1) {
199
+ this.state = ParseState.UNUSED;
200
+ return false;
201
+ }
202
+ option = this.rootOptionsByAlias.get(complexPathElement);
203
+ }
204
+
205
+ // if path doesn't refer to an option don't use it
206
+ if (option === undefined) {
207
+ return true;
208
+ }
209
+
210
+ // if we did find an option make sure we using the name and not the alias
211
+ complexPathElement = option.name;
212
+ } // complex option property navigation
213
+ else {
214
+ lookupPath = `${lookupPath}${option!.name}`;
215
+
216
+ // lazy creation of nested option paths to options
217
+ if (!this.optionLookupMapsByPath.has(lookupPath)) {
218
+ const properties = (option! as ComplexOption).properties;
219
+
220
+ if (properties === undefined) {
221
+ this.invalidArgument = {
222
+ name: optionPath,
223
+ reason: InvalidArgumentReason.UNKNOWN_PROPERTY,
224
+ };
225
+ this.state = ParseState.ERROR;
226
+ return false;
227
+ }
228
+
229
+ const lookupMap = new Map();
230
+ properties.forEach((option) => {
231
+ lookupMap.set(option.name, option);
232
+ if (option.shortAlias) {
233
+ lookupMap.set(option.shortAlias, option);
234
+ }
235
+ });
236
+ this.optionLookupMapsByPath.set(lookupPath, lookupMap);
237
+ }
238
+ option = this.optionLookupMapsByPath.get(lookupPath)!.get(
239
+ complexPathElement,
240
+ );
241
+
242
+ // now we are parsing properties of complex options it is an error if we cannot match an option name or alias
243
+ if (option === undefined) {
244
+ this.invalidArgument = {
245
+ name: optionPath,
246
+ reason: InvalidArgumentReason.UNKNOWN_PROPERTY,
247
+ };
248
+ this.state = ParseState.ERROR;
249
+ return false;
250
+ }
251
+
252
+ // ensure we convert to option name if lookup was via option alias
253
+ complexPathElement = option.name;
254
+ }
255
+
256
+ // check index size
257
+ let arrayIndex: number | undefined;
258
+ if (arrayIndexString !== undefined) {
259
+ const parsedArrayIndex = parseInt(arrayIndexString);
260
+ if (Number.isInteger(parsedArrayIndex)) {
261
+ if (parsedArrayIndex > MAXIMUM_ARGUMENT_ARRAY_SIZE) {
262
+ this.invalidArgument = {
263
+ argument: option,
264
+ name: option!.name,
265
+ reason: InvalidArgumentReason.ARRAY_SIZE_EXCEEDED,
266
+ };
267
+ this.state = ParseState.ERROR;
268
+ return false;
269
+ }
270
+ arrayIndex = parsedArrayIndex;
271
+ }
272
+ }
273
+
274
+ // check for indexing when not allowed
275
+ if (!option.isArray && (arrayIndex !== undefined)) {
276
+ this.invalidArgument = {
277
+ argument: option,
278
+ name: optionPath,
279
+ reason: InvalidArgumentReason.ILLEGAL_MULTIPLE_VALUES,
280
+ };
281
+ this.state = ParseState.ERROR;
282
+ return false;
283
+ }
284
+
285
+ if (
286
+ (currentValue as PopulatedArgumentValues)[complexPathElement] ===
287
+ undefined
288
+ ) {
289
+ // ensure we have a placeholder value
290
+ if (option.isArray) {
291
+ currentValue =
292
+ (currentValue as PopulatedArgumentValues)[complexPathElement] =
293
+ [];
294
+
295
+ if (isComplexOption(option)) {
296
+ // if we are retrieving a property by name only use the last entry in the array by default
297
+ // (this is the scenario when array indices are implicit)
298
+ let index = 0;
299
+ if (arrayIndex !== undefined) {
300
+ index = arrayIndex;
301
+ }
302
+ currentValue = currentValue[index] = {};
303
+ }
304
+ } else if (isComplexOption(option)) {
305
+ currentValue =
306
+ (currentValue as PopulatedArgumentValues)[complexPathElement] =
307
+ {};
308
+ }
309
+ } else {
310
+ currentValue = (currentValue as PopulatedArgumentValues)[
311
+ complexPathElement
312
+ ] as PopulatedArgumentValues;
313
+ if (Array.isArray(currentValue)) {
314
+ // if we are retrieving a property by name only use the last entry in the array by default
315
+ // (this is the scenario when array indices are implicit)
316
+ let index: number = currentValue.length - 1;
317
+ if (arrayIndex !== undefined) {
318
+ index = arrayIndex;
319
+ }
320
+
321
+ if ((currentValue[index] === undefined) && isComplexOption(option)) {
322
+ // ensure we have a placeholder value
323
+ currentValue = currentValue[index] = {};
324
+ } else {
325
+ currentValue = currentValue[index];
326
+ }
327
+ }
328
+ }
329
+
330
+ // add to the path elements
331
+ complexAndArrayPathElements.push(complexPathElement);
332
+ if (arrayIndex !== undefined) {
333
+ complexAndArrayPathElements.push(arrayIndex);
334
+ }
335
+ }
336
+
337
+ // if path doesn't refer to an option don't use it
338
+ if (option === undefined) {
339
+ return true;
340
+ }
341
+
342
+ // check if path ends up referring to a non-primitive
343
+ if (isComplexOption(option)) {
344
+ this.invalidArgument = {
345
+ argument: option,
346
+ name: option.name,
347
+ reason: InvalidArgumentReason.OPTION_IS_COMPLEX,
348
+ };
349
+ this.state = ParseState.ERROR;
350
+ return false;
351
+ }
352
+
353
+ this.currentOptionPath = complexAndArrayPathElements;
354
+ this.currentOption = option;
355
+
356
+ this.state = ParseState.OPTION_VALUE_EXPECTED;
357
+
358
+ return true;
359
+ }
360
+
361
+ /**
362
+ * Attempt to set the value for the current {@link Option}.
363
+ *
364
+ * @param value the value to attempt to set.
365
+ *
366
+ * @return `false` if there is a problem populating the value, in which case the {@link state} will be set to
367
+ * {@link ParseState.ERROR} and {@link invalidArgument} will be populated.
368
+ */
369
+ setOptionValue(value: ArgumentSingleValueType): boolean {
370
+ let optionValue:
371
+ | Array<PopulatedArgumentValues>
372
+ | PopulatedArgumentValues
373
+ | PopulatedArgumentValueType = this.populatedArgumentValues;
374
+ const optionPath = this.currentOptionPath as Array<string | number>;
375
+
376
+ // traverse the current populated values using the validated option path elements
377
+ for (let i = 0; i < optionPath.length; i++) {
378
+ const pathElement = optionPath[i];
379
+
380
+ // if on last path element, set the value
381
+ if (i === optionPath.length - 1) {
382
+ if (typeof pathElement === "string") {
383
+ const currentValue =
384
+ (optionValue as PopulatedArgumentValues)[pathElement as string];
385
+
386
+ // if we haven't populated the argument value before...
387
+ if (currentValue === undefined) {
388
+ (optionValue as PopulatedArgumentValues)[pathElement as string] =
389
+ value;
390
+ } // if we have already populated the argument value as an array...
391
+ else if (Array.isArray(currentValue)) {
392
+ if (currentValue.length === MAXIMUM_ARGUMENT_ARRAY_SIZE) {
393
+ this.invalidArgument = {
394
+ argument: this.currentOption,
395
+ name: this.currentOption!.name,
396
+ reason: InvalidArgumentReason.ARRAY_SIZE_EXCEEDED,
397
+ };
398
+ this.state = ParseState.ERROR;
399
+ return false;
400
+ }
401
+ // push the new value
402
+ (currentValue as Array<PopulatedArgumentSingleValueType>).push(
403
+ value,
404
+ );
405
+ } // else the value to add would make an array where it is not allowed
406
+ else {
407
+ this.invalidArgument = {
408
+ argument: this.currentOption,
409
+ name: this.currentOption!.name,
410
+ reason: InvalidArgumentReason.ILLEGAL_MULTIPLE_VALUES,
411
+ };
412
+ this.state = ParseState.ERROR;
413
+ return false;
414
+ }
415
+ } else {
416
+ // set the new value on the array
417
+ (optionValue as Array<
418
+ PopulatedArgumentValueType | PopulatedArgumentValues
419
+ >)[pathElement as number] = value;
420
+ }
421
+ } // otherwise continue to navigate the path
422
+ else {
423
+ if (typeof pathElement === "string") {
424
+ if (Array.isArray(optionValue)) {
425
+ optionValue = (optionValue as Array<PopulatedArgumentValues>)[
426
+ optionValue.length - 1
427
+ ][pathElement as string] as
428
+ | Array<PopulatedArgumentValues>
429
+ | PopulatedArgumentValues
430
+ | PopulatedArgumentValueType;
431
+ } else {
432
+ optionValue =
433
+ (optionValue as PopulatedArgumentValues)[pathElement as string] as
434
+ | Array<PopulatedArgumentValues>
435
+ | PopulatedArgumentValues
436
+ | PopulatedArgumentValueType;
437
+ }
438
+ } else {
439
+ optionValue = (optionValue as Array<
440
+ PopulatedArgumentSingleValueType | PopulatedArgumentValues
441
+ >)[pathElement as number];
442
+ }
443
+ }
444
+ }
445
+
446
+ // Update the state
447
+ this.currentOption = undefined;
448
+ this.currentOptionPath = undefined;
449
+ this.state = ParseState.EMPTY;
450
+
451
+ return true;
452
+ }
453
+
454
+ /**
455
+ * Attempt to set the value for the current {@link Positional}.
456
+ * If there is a problem populating the value the {@link state} will be set to
457
+ * {@link ParseState.ERROR} and {@link invalidArgument} will be populated.
458
+ *
459
+ * @param value the value to attempt to set.
460
+ */
461
+ setPositionalValue(value: ArgumentSingleValueType): boolean {
462
+ const positional = this.subCommand.positionals[this.currentPositionalIndex];
463
+ const currentValue = this.populatedArgumentValues[positional.name];
464
+
465
+ // if we haven't populated the argument value before...
466
+ if (currentValue === undefined) {
467
+ if (positional.isVarargMultiple) {
468
+ this.populatedArgumentValues[positional.name] = [value];
469
+ } else {
470
+ this.populatedArgumentValues[positional.name] = value;
471
+ }
472
+ } // if we have already populated the argument value as an array...
473
+ else if (Array.isArray(currentValue)) {
474
+ if (currentValue.length === MAXIMUM_ARGUMENT_ARRAY_SIZE) {
475
+ this.invalidArgument = {
476
+ argument: positional,
477
+ name: positional.name,
478
+ reason: InvalidArgumentReason.ARRAY_SIZE_EXCEEDED,
479
+ };
480
+ this.state = ParseState.ERROR;
481
+
482
+ return false;
483
+ }
484
+ this.populatedArgumentValues[positional.name] = [
485
+ ...currentValue as Array<PopulatedArgumentSingleValueType>,
486
+ value,
487
+ ] as Array<PopulatedArgumentSingleValueType>;
488
+ }
489
+
490
+ // Update the state
491
+ if (!positional.isVarargMultiple) {
492
+ this.currentPositionalIndex += 1;
493
+ }
494
+ this.state = ParseState.EMPTY;
495
+
496
+ return true;
497
+ }
498
+
499
+ /**
500
+ * Attempt to parse the next argument and update the state of this context based on the result.
501
+ *
502
+ * @param potentialArg the argument to parse
503
+ */
504
+ parse(potentialArg: string) {
505
+ if (this.state === ParseState.OPTION_VALUE_EXPECTED) {
506
+ // If the potential arg is a potential value, check if it is a non-boolean value
507
+ // and if so, see if the current option is boolean. In this scenario prefer to
508
+ // assume the boolean value was implicitly `true` and treat
509
+ // the potential arg as a following option arg or positional value
510
+ if (
511
+ (potentialArg !== "true") && (potentialArg !== "false") &&
512
+ this.currentOption!.type === ArgumentValueTypeName.BOOLEAN
513
+ ) {
514
+ // Set the value to implicit value of true and return ony error.
515
+ if (!this.setOptionValue("true")) {
516
+ return;
517
+ }
518
+
519
+ // Because of the implicit value, the current potential arg is either a new option arg or positional value
520
+ // and it will be processed in the `ParseState.EMPTY` logic further below.
521
+ } else {
522
+ // Treat the potential arg as the value for the current option and set the value.
523
+ this.setOptionValue(potentialArg);
524
+
525
+ // Return regardless.
526
+ return;
527
+ }
528
+ }
529
+ // Now either expecting option argument (`--x`, `-x`, `--x=y`, `-x=y`, `--x.z[1]`, `--x.z[1]=y` etc.) or positional value next.
530
+ if (this.state === ParseState.EMPTY) {
531
+ // Look for an option argument first.
532
+ if (potentialArg.startsWith("-")) {
533
+ let potentialOptionPath = potentialArg;
534
+ let potentialOptionValue;
535
+
536
+ // check for option AND value
537
+ if (potentialOptionPath.includes("=")) {
538
+ [potentialOptionPath, potentialOptionValue] = potentialOptionPath
539
+ .split(/=(.*)/);
540
+
541
+ // check for illegal syntax e.g. --optionName=
542
+ if (potentialOptionValue === "") {
543
+ this.state = ParseState.ERROR;
544
+ if (potentialOptionPath.startsWith("--")) {
545
+ potentialOptionPath = potentialOptionPath.slice(2);
546
+ } else {
547
+ potentialOptionPath = potentialOptionPath.slice(1);
548
+ }
549
+
550
+ this.invalidArgument = {
551
+ reason: InvalidArgumentReason.MISSING_VALUE,
552
+ name: potentialOptionPath,
553
+ };
554
+ return;
555
+ }
556
+ }
557
+
558
+ // Parse the option path and return if it failed
559
+ if (!this.#parseOptionPath(potentialOptionPath)) {
560
+ return;
561
+ }
562
+
563
+ // Check if the option path did refer to an option
564
+ if (this.currentOption !== undefined) {
565
+ // try to set the value if we have one
566
+ if (potentialOptionValue !== undefined) {
567
+ // set the value
568
+ this.setOptionValue(potentialOptionValue);
569
+
570
+ // return regardless
571
+ return;
572
+ }
573
+ // otherwise we just got the option and the next arg will be a value
574
+ return;
575
+ }
576
+
577
+ // As no option was found, in the logic below, the potential arg will be either assigned as a positional value or returned as unused.
578
+ }
579
+
580
+ // Not an option argument so try as positional value.
581
+ if (this.currentPositionalIndex < this.subCommand.positionals.length) {
582
+ // Treat the potential arg as the value for the current positional and set the value.
583
+ this.setPositionalValue(potentialArg);
584
+
585
+ // Return regardless of success or not.
586
+ return;
587
+ }
588
+
589
+ // If neither option argument nor positional value so treat as unused.
590
+ this.state = ParseState.UNUSED;
591
+ return;
592
+ }
593
+ throw new Error(`Unexpected parse state: ${this.state}`);
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Populate {@link PopulatedArgumentValues} for the provided {@link SubCommand} using the provided potential args.
599
+ *
600
+ * @param subCommand the {@link SubCommand} for which {@link PopulatedArgumentValues} values should be populated.
601
+ * @param potentialArgs the potential args to use for population.
602
+ * @param defaultValues optional default {@link ArgumentValues} to use for population before parsing the provided arguments.
603
+ */
604
+ export default function populateSubCommandValues(
605
+ subCommand: SubCommand,
606
+ potentialArgs: ReadonlyArray<string>,
607
+ defaultValues: PopulatedArgumentValues | undefined,
608
+ ): SubCommandValuePopulationResult {
609
+ logger.debug(() => {
610
+ const message =
611
+ `Populating values for sub-command: '${subCommand.name}' using potential args: ${
612
+ potentialArgs.join(" ")
613
+ }`;
614
+ if (defaultValues !== undefined) {
615
+ return `${message} and configured values: ${
616
+ JSON.stringify(defaultValues)
617
+ }`;
618
+ }
619
+ return message;
620
+ });
621
+
622
+ const parseContext = new ParseContext(subCommand);
623
+ const unusedArgs: Array<string> = [];
624
+
625
+ for (let i = 0; i < potentialArgs.length; i++) {
626
+ const potentialArg = potentialArgs[i];
627
+
628
+ // parse the next arg
629
+ parseContext.parse(potentialArg);
630
+
631
+ // check if we had a parsing error, record the error, flush the remaining args and stop parsing
632
+ if (parseContext.state === ParseState.ERROR) {
633
+ if (i + 1 < potentialArgs.length) {
634
+ unusedArgs.push(...potentialArgs.slice(i + 1));
635
+ }
636
+ break;
637
+ }
638
+ // check if the arg was unused, save it and reset state for the next arg
639
+ if (parseContext.state === ParseState.UNUSED) {
640
+ unusedArgs.push(potentialArg);
641
+ parseContext.state = ParseState.EMPTY;
642
+ }
643
+ }
644
+
645
+ // Check if the last arg specified an option but no value
646
+ if (parseContext.state === ParseState.OPTION_VALUE_EXPECTED) {
647
+ // special case if the last arg was for a boolean option where the value was implicitly `true`
648
+ if (parseContext.currentOption!.type === ArgumentValueTypeName.BOOLEAN) {
649
+ parseContext.setOptionValue("true");
650
+ } else {
651
+ parseContext.invalidArgument = {
652
+ argument: parseContext.currentOption,
653
+ reason: InvalidArgumentReason.MISSING_VALUE,
654
+ name: potentialArgs[potentialArgs.length - 1],
655
+ };
656
+ }
657
+ }
658
+
659
+ if (parseContext.invalidArgument) {
660
+ return {
661
+ populatedArgumentValues: parseContext.populatedArgumentValues,
662
+ unusedArgs: unusedArgs,
663
+ invalidArgument: parseContext.invalidArgument,
664
+ };
665
+ }
666
+
667
+ // merge the configured values with the parsed values
668
+ let populatedArgumentValues = parseContext.populatedArgumentValues;
669
+ if (defaultValues) {
670
+ populatedArgumentValues = argumentValueMerge(
671
+ populatedArgumentValues,
672
+ defaultValues,
673
+ );
674
+ }
675
+
676
+ return {
677
+ populatedArgumentValues,
678
+ unusedArgs: unusedArgs,
679
+ };
680
+ }