LotSizingDataModel.Checker.Cli 2.0.1
Command-line interface for solution checking and validation workflows.
Loading...
Searching...
No Matches
CliOptionsParser.cs
Go to the documentation of this file.
1using System.Globalization;
2using LotSizingDataModel.Checker.Common;
3
5
6internal static class CliOptionsParser
7{
8 private static readonly CultureInfo ParsingCulture =
9 CultureInfo.InvariantCulture;
10
11 public static CliParseResult Parse(string[] args)
12 {
13 ArgumentNullException.ThrowIfNull(args);
14
15 var options = new CliOptions();
16 string? inputDirectory = null;
17 bool endOfOptions = false;
18
19 for (int index = 0; index < args.Length; index++)
20 {
21 string argument = args[index];
22
23 if (endOfOptions)
24 {
25 if (!TrySetInputDirectory(argument, ref inputDirectory, out string? error))
26 {
27 return Failure(error!);
28 }
29
30 continue;
31 }
32
33 if (argument == "--")
34 {
35 endOfOptions = true;
36 continue;
37 }
38
39 if (argument is "-h" or "--help")
40 {
41 return new CliParseResult { ShowHelp = true };
42 }
43
44 if (argument == "--version")
45 {
46 return new CliParseResult { ShowVersion = true };
47 }
48
49 if (!argument.StartsWith("-", StringComparison.Ordinal) ||
50 argument == "-")
51 {
52 if (!TrySetInputDirectory(argument, ref inputDirectory, out string? error))
53 {
54 return Failure(error!);
55 }
56
57 continue;
58 }
59
60 SplitOption(argument, out string optionName, out string? inlineValue);
61
62 switch (optionName)
63 {
64 case "-o":
65 case "--output":
66 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? output, out string? outputError))
67 {
68 return Failure(outputError!);
69 }
70
71 options.OutputDirectory = output;
72 break;
73
74 case "-j":
75 case "--parallelism":
76 if (!TryReadPositiveInt(args, ref index, inlineValue, optionName, out int parallelism, out string? parallelismError))
77 {
78 return Failure(parallelismError!);
79 }
80
81 options.MaxDegreeOfParallelism = parallelism;
82 break;
83
84 case "--level":
85 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? levelValue, out string? levelError))
86 {
87 return Failure(levelError!);
88 }
89
90 if (!TryParseLevel(levelValue!, out SolutionCheckLevel level))
91 {
92 return Failure(
93 "Invalid --level value. Expected structural, feasibility, or full.");
94 }
95
96 options.CheckOptions.Level = level;
97 break;
98
99 case "--search-pattern":
100 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? searchPattern, out string? patternError))
101 {
102 return Failure(patternError!);
103 }
104
105 options.SearchPattern = searchPattern!;
106 break;
107
108 case "--result-id":
109 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? resultId, out string? resultIdError))
110 {
111 return Failure(resultIdError!);
112 }
113
114 options.KnownResultId = resultId;
115 break;
116
117 case "--result-name-contains":
118 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? resultName, out string? resultNameError))
119 {
120 return Failure(resultNameError!);
121 }
122
123 options.KnownResultNameContains = resultName;
124 break;
125
126 case "--feasibility-tolerance":
127 case "--feasibility-tol":
128 if (!TryReadTolerance(args, ref index, inlineValue, optionName, out double feasibilityTolerance, out string? feasibilityError))
129 {
130 return Failure(feasibilityError!);
131 }
132
133 options.CheckOptions.FeasibilityTolerance = feasibilityTolerance;
134 break;
135
136 case "--zero-tolerance":
137 case "--zero-tol":
138 if (!TryReadTolerance(args, ref index, inlineValue, optionName, out double zeroTolerance, out string? zeroError))
139 {
140 return Failure(zeroError!);
141 }
142
143 options.CheckOptions.ZeroTolerance = zeroTolerance;
144 break;
145
146 case "--integrality-tolerance":
147 case "--integrality-tol":
148 if (!TryReadTolerance(args, ref index, inlineValue, optionName, out double integralityTolerance, out string? integralityError))
149 {
150 return Failure(integralityError!);
151 }
152
153 options.CheckOptions.IntegralityTolerance = integralityTolerance;
154 break;
155
156 case "--objective-absolute-tolerance":
157 case "--objective-abs-tol":
158 if (!TryReadTolerance(args, ref index, inlineValue, optionName, out double objectiveAbsoluteTolerance, out string? objectiveAbsoluteError))
159 {
160 return Failure(objectiveAbsoluteError!);
161 }
162
163 options.CheckOptions.ObjectiveAbsoluteTolerance = objectiveAbsoluteTolerance;
164 break;
165
166 case "--objective-relative-tolerance":
167 case "--objective-rel-tol":
168 if (!TryReadTolerance(args, ref index, inlineValue, optionName, out double objectiveRelativeTolerance, out string? objectiveRelativeError))
169 {
170 return Failure(objectiveRelativeError!);
171 }
172
173 options.CheckOptions.ObjectiveRelativeTolerance = objectiveRelativeTolerance;
174 break;
175
176 case "--max-issues":
177 if (!TryReadNonNegativeInt(args, ref index, inlineValue, optionName, out int maximumIssues, out string? maximumIssuesError))
178 {
179 return Failure(maximumIssuesError!);
180 }
181
182 options.ReportOptions.MaximumDetailedIssues = maximumIssues;
183 break;
184
185 case "--evaluator-name":
186 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? evaluatorName, out string? evaluatorNameError))
187 {
188 return Failure(evaluatorNameError!);
189 }
190
191 options.VerificationOptions.EvaluatorName = evaluatorName!;
192 break;
193
194 case "--evaluator-version":
195 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? evaluatorVersion, out string? evaluatorVersionError))
196 {
197 return Failure(evaluatorVersionError!);
198 }
199
200 options.VerificationOptions.EvaluatorVersion = evaluatorVersion!;
201 break;
202
203 case "--no-recursive":
204 if (!RejectInlineValue(inlineValue, optionName, out string? noRecursiveError))
205 {
206 return Failure(noRecursiveError!);
207 }
208
209 options.SearchSubdirectories = false;
210 break;
211
212 case "--strict-xml":
213 if (!RejectInlineValue(inlineValue, optionName, out string? strictXmlError))
214 {
215 return Failure(strictXmlError!);
216 }
217
218 options.IgnoreNonLotSizingInstanceXml = false;
219 break;
220
221 case "--no-reports":
222 if (!RejectInlineValue(inlineValue, optionName, out string? noReportsError))
223 {
224 return Failure(noReportsError!);
225 }
226
227 options.WriteReports = false;
228 break;
229
230 case "--no-progress":
231 case "--quiet":
232 if (!RejectInlineValue(inlineValue, optionName, out string? noProgressError))
233 {
234 return Failure(noProgressError!);
235 }
236
237 options.ShowProgress = false;
238 break;
239
240 case "--continue-after-structural-errors":
241 if (!RejectInlineValue(inlineValue, optionName, out string? continueError))
242 {
243 return Failure(continueError!);
244 }
245
246 options.CheckOptions.ContinueAfterStructuralErrors = true;
247 break;
248
249 case "--include-disabled-constraints":
250 if (!RejectInlineValue(inlineValue, optionName, out string? disabledError))
251 {
252 return Failure(disabledError!);
253 }
254
255 options.CheckOptions.IgnoreDisabledConstraints = false;
256 break;
257
258 case "--no-apply-evaluation":
259 if (!RejectInlineValue(inlineValue, optionName, out string? applyError))
260 {
261 return Failure(applyError!);
262 }
263
264 options.VerificationOptions.ApplyToSolutionEvaluation = false;
265 break;
266
267 case "--no-update-known-result":
268 if (!RejectInlineValue(inlineValue, optionName, out string? updateError))
269 {
270 return Failure(updateError!);
271 }
272
273 options.VerificationOptions.UpdateKnownResultFeasibility = false;
274 break;
275
276 case "--no-promote-known-result":
277 if (!RejectInlineValue(inlineValue, optionName, out string? promoteError))
278 {
279 return Failure(promoteError!);
280 }
281
282 options.VerificationOptions.PromoteFullyVerifiedKnownResult = false;
283 break;
284
285 case "--no-info":
286 if (!RejectInlineValue(inlineValue, optionName, out string? infoError))
287 {
288 return Failure(infoError!);
289 }
290
291 options.ReportOptions.IncludeInformationIssues = false;
292 break;
293
294 case "--no-warnings":
295 if (!RejectInlineValue(inlineValue, optionName, out string? warningsError))
296 {
297 return Failure(warningsError!);
298 }
299
300 options.ReportOptions.IncludeWarningIssues = false;
301 break;
302
303 default:
304 return Failure($"Unknown option '{optionName}'. Use --help for usage.");
305 }
306 }
307
308 if (string.IsNullOrWhiteSpace(inputDirectory))
309 {
310 return Failure("An input directory is required. Use --help for usage.");
311 }
312
313 options.InputDirectory = inputDirectory;
314
315 try
316 {
317 options.BuildCampaignOptions();
318 }
319 catch (Exception exception) when (
320 exception is ArgumentException or InvalidOperationException)
321 {
322 return Failure(exception.Message);
323 }
324
325 return new CliParseResult { Options = options };
326 }
327
328 private static void SplitOption(
329 string argument,
330 out string optionName,
331 out string? inlineValue)
332 {
333 int equalsIndex = argument.IndexOf('=');
334
335 if (equalsIndex < 0)
336 {
337 optionName = argument;
338 inlineValue = null;
339 return;
340 }
341
342 optionName = argument[..equalsIndex];
343 inlineValue = argument[(equalsIndex + 1)..];
344 }
345
346 private static bool TrySetInputDirectory(
347 string value,
348 ref string? inputDirectory,
349 out string? error)
350 {
351 if (inputDirectory is not null)
352 {
353 error =
354 $"Unexpected positional argument '{value}'. Only one input directory is allowed.";
355 return false;
356 }
357
358 inputDirectory = value;
359 error = null;
360 return true;
361 }
362
363 private static bool TryReadValue(
364 string[] args,
365 ref int index,
366 string? inlineValue,
367 string optionName,
368 out string? value,
369 out string? error)
370 {
371 if (inlineValue is not null)
372 {
373 if (inlineValue.Length == 0)
374 {
375 value = null;
376 error = $"Option '{optionName}' requires a non-empty value.";
377 return false;
378 }
379
380 value = inlineValue;
381 error = null;
382 return true;
383 }
384
385 if (index + 1 >= args.Length)
386 {
387 value = null;
388 error = $"Option '{optionName}' requires a value.";
389 return false;
390 }
391
392 index++;
393 value = args[index];
394
395 if (string.IsNullOrWhiteSpace(value))
396 {
397 error = $"Option '{optionName}' requires a non-empty value.";
398 return false;
399 }
400
401 error = null;
402 return true;
403 }
404
405 private static bool TryReadPositiveInt(
406 string[] args,
407 ref int index,
408 string? inlineValue,
409 string optionName,
410 out int value,
411 out string? error)
412 {
413 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? text, out error))
414 {
415 value = 0;
416 return false;
417 }
418
419 if (!int.TryParse(text, NumberStyles.Integer, ParsingCulture, out value) || value < 1)
420 {
421 error = $"Option '{optionName}' requires an integer greater than or equal to 1.";
422 return false;
423 }
424
425 return true;
426 }
427
428 private static bool TryReadNonNegativeInt(
429 string[] args,
430 ref int index,
431 string? inlineValue,
432 string optionName,
433 out int value,
434 out string? error)
435 {
436 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? text, out error))
437 {
438 value = 0;
439 return false;
440 }
441
442 if (!int.TryParse(text, NumberStyles.Integer, ParsingCulture, out value) || value < 0)
443 {
444 error = $"Option '{optionName}' requires a non-negative integer.";
445 return false;
446 }
447
448 return true;
449 }
450
451 private static bool TryReadTolerance(
452 string[] args,
453 ref int index,
454 string? inlineValue,
455 string optionName,
456 out double value,
457 out string? error)
458 {
459 if (!TryReadValue(args, ref index, inlineValue, optionName, out string? text, out error))
460 {
461 value = 0.0;
462 return false;
463 }
464
465 if (!double.TryParse(text, NumberStyles.Float, ParsingCulture, out value) ||
466 !double.IsFinite(value) ||
467 value < 0.0)
468 {
469 error =
470 $"Option '{optionName}' requires a finite non-negative number using invariant notation.";
471 return false;
472 }
473
474 return true;
475 }
476
477 private static bool TryParseLevel(
478 string value,
479 out SolutionCheckLevel level)
480 {
481 switch (value.Trim().ToLowerInvariant())
482 {
483 case "structural":
484 case "structure":
485 level = SolutionCheckLevel.Structural;
486 return true;
487
488 case "feasibility":
489 case "feasible":
490 level = SolutionCheckLevel.Feasibility;
491 return true;
492
493 case "full":
494 level = SolutionCheckLevel.Full;
495 return true;
496
497 default:
498 level = default;
499 return false;
500 }
501 }
502
503 private static bool RejectInlineValue(
504 string? inlineValue,
505 string optionName,
506 out string? error)
507 {
508 if (inlineValue is null)
509 {
510 error = null;
511 return true;
512 }
513
514 error = $"Flag '{optionName}' does not accept a value.";
515 return false;
516 }
517
518 private static CliParseResult Failure(string message)
519 {
520 return new CliParseResult { ErrorMessage = message };
521 }
522}
static CliParseResult Parse(string[] args)