getopt.net
A port of getopt in pure C#.
GetOpt.cs
Go to the documentation of this file.
1using System;
2
3namespace getopt.net {
4
5 using System.IO;
6 using System.Text.RegularExpressions;
7
15 public partial class GetOpt {
16
20 public const char MissingArgChar = '?';
21
25 public const char InvalidOptChar = '!';
26
30 public const char NonOptChar = (char)1;
31
35 public const string DoubleDash = "--";
36
41 public const char SingleDash = '-';
42
47 public const char SingleSlash = '/';
48
52 public const char WinArgSeparator = ':';
53
57 public const char GnuArgSeparator = '=';
58
62 public const string ArgSplitRegex = @"([\s]|[=])";
63
68 public const char SingleAtSymbol = '@';
69
73 public Option[] Options { get; set; } = Array.Empty<Option>();
74
78 public string? ShortOpts { get; set; } = null;
79
84 public bool DoubleDashStopsParsing { get; set; } = true;
85
86 private string[] m_appArgs = Array.Empty<string>();
87
91 public string[] AppArgs {
92 get => m_appArgs;
93 set => m_appArgs = value;
94 }
95
100 public bool OnlyShortOpts { get; set; } = false;
101
106 public bool IgnoreEmptyOptions { get; set; } = true;
107
115 public bool IgnoreMissingArgument { get; set; } = false;
116
124 public bool IgnoreInvalidOptions { get; set; } = true;
125
130 public bool IgnoreEmptyAppArgs { get; set; } = true;
131
139 public bool StopParsingOptions { get; set; } = false;
140
149 public bool AllowWindowsConventions { get; set; } = false;
150
160 public bool AllowPowershellConventions { get; set; } = false;
161
178 public bool AllowParamFiles { get; set; } = false;
179
180
188 set {
190 }
191 }
192
197
202 public GetOpt() { }
203
210 public GetOpt(string[] appArgs, string shortOpts, params Option[] options) {
211 AppArgs = appArgs;
212 ShortOpts = shortOpts;
213 Options = options;
214 }
215
219 protected int m_currentIndex = 0;
220
224 protected int m_optPosition = 1; // this applies to short opts only, where [0] == '-'
225
230#if NET7_0_OR_GREATER
231 [GeneratedRegex(ArgSplitRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled)]
232 protected static partial Regex ArgumentSplitter();
233#else
234 protected static Regex ArgumentSplitter() => new(ArgSplitRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
235#endif
236
240 protected void ResetOptPosition() => m_optPosition = 1;
241
255 protected bool MustReturnChar1() => ShortOpts?.Length > 0 && ShortOpts[0] == '-';
256
264 protected bool MustStopParsing() => ShortOpts?.Length > 0 && ShortOpts[0] == '+' || Environment.GetEnvironmentVariable("POSIXLY_CORRECT") is not null;
265
272 public int GetNextOpt(out string? outOptArg) {
273 if (AppArgs.Length == 0) {
274 if (!IgnoreEmptyAppArgs) {
275 throw new ParseException("No arguments found for parsing!");
276 } else {
277 outOptArg = null;
278 return -1;
279 }
280 }
281
282 outOptArg = null; // pre-set this here so we don't have to set it during every condition
283
284 if (CurrentIndex >= AppArgs.Length) { return -1; }
285
286 if (string.IsNullOrEmpty(AppArgs[m_currentIndex])) {
287 if (!IgnoreEmptyOptions) {
288 throw new ParseException("Encountered null or empty argument!");
289 } else { return 0; }
290 } else if (DoubleDashStopsParsing && AppArgs[CurrentIndex].Equals(DoubleDash, StringComparison.InvariantCultureIgnoreCase)) {
292 StopParsingOptions = true;
293 return GetNextOpt(out outOptArg);
294 }
295
296 // Check here if StopParsingOptions is true;
297 // if so, then simply return NonOptChar and set outOptArg to the value of the argument
298 if (StopParsingOptions) {
299 outOptArg = AppArgs[CurrentIndex];
301 return NonOptChar;
302 }
303
304 // Now check if the current argument is a paramfile argument
305 if (IsParamFileArg(AppArgs[CurrentIndex], out var paramFile) && paramFile is not null) {
306 ReadParamFile(new FileInfo(paramFile));
308 return GetNextOpt(out outOptArg); // We don't need to pass this back to the application. Instead just continue on
309 }
310
312 if (Options.Length == 0) { throw new ParseException("Cannot parse long option! No option list provided!"); }
313 return ParseLongOption(out outOptArg);
314 } else if (IsShortOption(AppArgs[CurrentIndex])) {
315 // check if both arg lists are empty
316 if (string.IsNullOrEmpty(ShortOpts) && Options.Length == 0) { throw new ParseException("Cannot parse short option! No option list provided!"); }
317 return ParseShortOption(out outOptArg);
318 }
319
321 outOptArg = AppArgs[CurrentIndex];
323 if (MustReturnChar1()) { return NonOptChar; }
324 else { return InvalidOptChar; }
325 } else {
326 throw new ParseException(AppArgs[CurrentIndex], "Unexpected option argument!");
327 }
328 }
329
335 var optChar = GetNextOpt(out var optArg);
336
337 if (optArg is null) {
338 return new CommandOption(optChar);
339 }
340
341 if (bool.TryParse(optArg, out bool outBool)) {
342 return new CommandOption(optChar, outBool);
343 } else if (double.TryParse(optArg, out double outDouble)) {
344 return new CommandOption(optChar, outDouble);
345 } else if (float.TryParse(optArg, out float outFloat)) {
346 return new CommandOption(optChar, outFloat);
347 } else if (int.TryParse(optArg, out int outInt)) {
348 return new CommandOption(optChar, outInt);
349 }
350
351 return new CommandOption(optChar, optArg);
352 }
353
364 protected int ParseLongOption(out string? optArg) {
365 if (HasArgumentInOption(out var optName, out optArg)) {
366 AppArgs[m_currentIndex] = optName;
367 }
368
370
371 var nullableOpt = Options.FindOptionOrDefault(AppArgs[m_currentIndex]);
372 if (nullableOpt is null) {
375 throw new ParseException(AppArgs[m_currentIndex], "Invalid option found!");
376 }
377
378 return InvalidOptChar;
379 }
380
381 var opt = (Option)nullableOpt;
382 switch (opt.ArgumentType) {
383 case ArgumentType.Required:
384 if (optArg == null && (IsLongOption(AppArgs[CurrentIndex + 1]) || IsShortOption(AppArgs[CurrentIndex + 1]))) {
387 return MissingArgChar;
388 } else {
389 throw new ParseException(AppArgs[CurrentIndex], "Missing required argument!");
390 }
391 } else if (optArg != null) { break; }
392
393 optArg = AppArgs[CurrentIndex + 1];
394 if (MustStopParsing()) { // POSIX behaviour desired
395 m_currentIndex = AppArgs.Length;
396 break;
397 }
398
400 break;
401 case ArgumentType.None:
402 default: // this case will handle cases where developers carelessly cast integers to the enum type
403 optArg = null;
404 break;
405 case ArgumentType.Optional:
406 // DRY this off at some point
407 if (optArg == null && !IsLongOption(AppArgs[CurrentIndex + 1]) && !IsShortOption(AppArgs[CurrentIndex + 1])) {
408 optArg = AppArgs[CurrentIndex + 1];
410 }
411 break;
412 }
413
415 return opt.Value;
416 }
417
426 protected bool TryGetArgumentForShortOption(ref string? arg, out bool incrementCurrentIndex) {
427 incrementCurrentIndex = false; // pre-set this
428
429 if (m_optPosition + 1 < AppArgs[CurrentIndex].Length) {
430 arg = AppArgs[CurrentIndex].Substring(m_optPosition + 1);
431 incrementCurrentIndex = true;
432 return true;
433 }
434
435 if (CurrentIndex + 1 >= AppArgs.Length) {
436 return false;
437 }
438
440 arg = AppArgs[CurrentIndex + 1];
441
442 if (MustStopParsing()) {
443 m_currentIndex = AppArgs.Length; // POSIX behaviour desired
444 } else {
445 m_currentIndex += 2;
446 }
447
448 return true;
449 }
450
451 return false;
452 }
453
460 protected int ParseShortOption(out string? optArg) {
461 optArg = null;
462 var curOpt = AppArgs[CurrentIndex][m_optPosition];
463
464 bool incrementCurrentIndex = false;
465 var argType = ShortOptRequiresArg(curOpt);
466 if (argType is null) {
469 return InvalidOptChar;
470 } else if (argType is ArgumentType type) {
471 switch (type) {
472 default:
473 case ArgumentType.None:
474 if (AppArgs[CurrentIndex].Length > AppArgs[CurrentIndex].IndexOf(curOpt) + 1) {
476 return curOpt;
477 }
478 break;
479 case ArgumentType.Optional:
480 if (TryGetArgumentForShortOption(ref optArg, out incrementCurrentIndex)) {
482 if (incrementCurrentIndex) { m_currentIndex++; }
483 return curOpt;
484 }
485 break;
486 case ArgumentType.Required:
487 if (!TryGetArgumentForShortOption(ref optArg, out incrementCurrentIndex)) {
488 if (incrementCurrentIndex) { m_currentIndex++; }
490 else { throw new ParseException(curOpt.ToString(), "Missing argument for option!"); }
491 }
492 break;
493 }
494 }
495
498
499 return curOpt;
500 }
501
508 protected ArgumentType? ShortOptRequiresArg(char shortOpt) {
509 if (!string.IsNullOrEmpty(ShortOpts) && ShortOpts is not null) {
510 var posInStr = ShortOpts.IndexOf(shortOpt);
511 if (posInStr == -1) {
512 goto CheckLongOpt;
513 }
514
515 try {
516
517 char charToCheck;
518 if (posInStr < ShortOpts.Length - 1) {
519 charToCheck = ShortOpts[posInStr + 1];
520 } else {
521 charToCheck = ShortOpts[posInStr];
522 }
523
524 switch (charToCheck) {
525 case ':':
526 return ArgumentType.Required;
527 case ';':
528 return ArgumentType.Optional;
529 default:
530 return ArgumentType.None;
531 }
532 } catch {
533 goto CheckLongOpt;
534 }
535 }
536
537 CheckLongOpt:
538 if (Options.Length == 0) {
540 return null;
541 } else {
542 throw new ParseException(shortOpt.ToString(), "Invalid option list!");
543 }
544 }
545 var nullableOpt = Options.FindOptionOrDefault(shortOpt);
546
547 if (nullableOpt == null) {
549 shortOpt = InvalidOptChar;
550 return ArgumentType.None;
551 } else { throw new ParseException(shortOpt.ToString(), "Encountered unknown option!"); }
552 }
553
554 var opt = (Option)nullableOpt;
555 return opt.ArgumentType ?? ArgumentType.None;
556 }
557
564 protected bool HasArgumentInOption(out string optName, out string? argVal) {
565 var curArg = AppArgs[CurrentIndex];
566 var splitString = Array.Empty<string>();
567
569 // if we're allowing Windows conventions, we have to replace
570 // the first occurrence of ':' in the arg string with '='
571 var indexOfSeparator = curArg.IndexOf(WinArgSeparator);
572 if (indexOfSeparator != -1) {
573 curArg = $"{ curArg.Substring(0, indexOfSeparator) }{ GnuArgSeparator }{ curArg.Substring(indexOfSeparator + 1) }";
574 }
575 }
576
577 splitString = ArgumentSplitter().Split(curArg);
578
579 if (splitString.Length == 1) {
580 optName = StripDashes(true); // we can set this to true, because this method will only ever be called for long opts
581 argVal = null;
582 return false;
583 }
584
585 optName = splitString[0];
586#if NET6_0_OR_GREATER
587 argVal = string.Join("", splitString[2..]);
588#else
589 argVal = string.Join("", splitString.Skip(2));
590#endif
591 return true;
592 }
593
602 protected string StripDashes(bool isLongOpt) {
603 var curArg = AppArgs[m_currentIndex];
604
606 curArg.StartsWith(SingleSlash.ToString())) {
607 return curArg.Substring(1);
608 }
609
610 if (!curArg.StartsWith(DoubleDash) &&
611 !curArg.StartsWith(SingleDash.ToString())) {
612 return curArg;
613 }
614
615 if (isLongOpt && curArg.StartsWith(DoubleDash)) {
616 return curArg.Substring(2);
617 } else if (curArg.StartsWith(SingleDash.ToString())) {
618 return curArg.Substring(1);
619 }
620
621 return curArg;
622 }
623
629 protected bool ShallStopParsing(ref string arg) {
630 return !string.IsNullOrEmpty(arg) &&
631 arg.Equals("--", StringComparison.CurrentCultureIgnoreCase);
632 }
633
639 protected bool IsLongOption(string arg) {
640 if (string.IsNullOrEmpty(arg)) { return false; }
641
642 if (
644 arg.Length > 1 &&
645 arg[0] == SingleSlash &&
646 Options.Length != 0 &&
647 Options.Any(o => o.Name == arg.Split(WinArgSeparator, GnuArgSeparator, ' ').First().Substring(1)) // We only need this option when parsing options following Windows' conventions
648 ) { return true; }
649
650 // Check for Powershell-style arguments.
651 // Powershell arguments are weird and extra checks are needed.
652 // Powershell-style arguments would theoretically interfere with short opts,
653 // so a check to determine whether or not the option is found in Options is required.
654 if (
656 arg.Length > 1 &&
657 arg[0] == SingleDash &&
658 Options.Length != 0 &&
659 Options.Any(o => o.Name == arg.Split(WinArgSeparator, GnuArgSeparator, ' ').First().Substring(1)) // We only need this when parsing options following Powershell's conventions
660 // This parsing method is really similar to Windows option parsing...
661 ) { return true; }
662
663 return arg.Length > 2 &&
664 arg[0] == SingleDash &&
665 arg[1] == SingleDash;
666 }
667
673 protected bool IsShortOption(string arg) {
674 if (string.IsNullOrEmpty(arg)) { return false; }
675
676 if (
678 arg.Length > 1 &&
679 arg[0] == SingleSlash &&
680 arg[1] != SingleSlash
681 ) { return true; }
682
683 return arg.Length > 1 &&
684 arg[0] == SingleDash &&
685 arg[1] != SingleDash;
686 }
687
699 protected bool IsParamFileArg(string arg, out string? paramFile) {
700 paramFile = null; // pre-set this so we don't have to do it everywhere
701 if (string.IsNullOrEmpty(arg) || !AllowParamFiles || arg.Length < 2) { return false; }
702
703 if (arg[0] != SingleAtSymbol) { return false; }
704
705 arg = arg.TrimStart('@');
706
707 if (File.Exists(arg)) {
708 paramFile = arg;
709 }
710
711 return true;
712 }
713
718 protected void ReadParamFile(FileInfo paramFile) {
719 if (paramFile == null || !paramFile.Exists) { return; }
720
721 var lastIndex = AppArgs.Length;
722 var lines = File.ReadAllLines(paramFile.FullName);
723 Array.Resize(ref m_appArgs, lines.Length + AppArgs.Length);
724
725 for (int i = lastIndex, j = 0; i < m_appArgs.Length && j < lines.Length; i++, j++) {
726 if (string.IsNullOrEmpty(lines[j].Trim())) { continue; }
727 if (lines[j].Trim()[0] == '#') { continue; }
728
729 m_appArgs[i] = lines[j];
730 }
731 }
732 }
733}
734
Represents a single argument received via command-line options.
Definition: CommandOption.cs:8
GetOpt-like class for handling getopt-like command-line arguments in .net.
Definition: GetOpt.cs:15
const char GnuArgSeparator
The argument separator used by POSIX / GNU getopt.
Definition: GetOpt.cs:57
bool TryGetArgumentForShortOption(ref string? arg, out bool incrementCurrentIndex)
Attempts to retrieve the argument for the current short option.
Definition: GetOpt.cs:426
bool ShallStopParsing(ref string arg)
Gets a value indicating whether or not the parser shall stop here.
Definition: GetOpt.cs:629
static Regex ArgumentSplitter()
Compiled Regex.
bool IsParamFileArg(string arg, out string? paramFile)
Gets a value indicating whether or not a given option is a paramfile option. AllowParamFiles.
Definition: GetOpt.cs:699
bool AllExceptionsDisabled
Either enables or disabled exceptions entirely. For more specific control over exceptions,...
Definition: GetOpt.cs:186
bool HasArgumentInOption(out string optName, out string? argVal)
Determines whether or not the current option contains its argument with the string or not.
Definition: GetOpt.cs:564
bool OnlyShortOpts
Gets or sets a value indicating whether or not to only parse short options. Default:
Definition: GetOpt.cs:100
bool MustStopParsing()
If the first character of ShortOpts is '+' or the environment variable POSIXLY_CORRECT is set,...
bool IsShortOption(string arg)
Gets a value indicating whether or not the current string is/contains a (or more) short option
Definition: GetOpt.cs:673
string StripDashes(bool isLongOpt)
Strips leading dashes from strings.
Definition: GetOpt.cs:602
const char SingleSlash
A single slash. This is the char that is searched for when parsing arguments with the Windows convent...
Definition: GetOpt.cs:47
GetOpt()
Default constructor; it is recommended to use this constructor and to use brace-initialiser-lists to ...
Definition: GetOpt.cs:202
bool DoubleDashStopsParsing
Gets or sets a value indicating whether or not "--" stops parsing. Default:
Definition: GetOpt.cs:84
CommandOption GetNextOpt()
Gets the next option in the list, returning an object containing more detailled information about the...
Definition: GetOpt.cs:334
Option[] Options
An optional list of long options to go with the short options.
Definition: GetOpt.cs:73
const char WinArgSeparator
The argument separator used by Windows.
Definition: GetOpt.cs:52
ArgumentType? ShortOptRequiresArg(char shortOpt)
Gets a value indicating whether or not a short option requires an argument.
Definition: GetOpt.cs:508
bool AllowParamFiles
Gets or sets a value indicating whether or not parameter files are accepted as a valid form of input.
Definition: GetOpt.cs:178
void ReadParamFile(FileInfo paramFile)
Reads the incoming param file and adds the contents to AppArgs
Definition: GetOpt.cs:718
bool IgnoreEmptyOptions
Gets or sets a value indicating whether or not to ignore empty values. Default:
Definition: GetOpt.cs:106
bool IgnoreInvalidOptions
Gets or sets a value indicating whether or not invalid arguments should be ignored or not....
Definition: GetOpt.cs:124
const char NonOptChar
The character that is returned when a non-option value is encountered and it is not the argument to a...
Definition: GetOpt.cs:30
int ParseLongOption(out string? optArg)
Parses long options
Definition: GetOpt.cs:364
int ParseShortOption(out string? optArg)
Parses a single short option.
Definition: GetOpt.cs:460
int m_currentIndex
The current index while traversing AppArgs
Definition: GetOpt.cs:219
const string ArgSplitRegex
The regex used by ArgumentSplitter to split arguments into a key-value pair.
Definition: GetOpt.cs:62
bool IgnoreMissingArgument
Gets or sets a value indicating whether or not to ignore missing arguments. Default:
Definition: GetOpt.cs:115
string[] AppArgs
Gets or sets the arguments to parse.
Definition: GetOpt.cs:91
bool StopParsingOptions
Gets or sets a value indicating whether or not option parsing shall stop or not. Default:
Definition: GetOpt.cs:139
bool AllowWindowsConventions
Gets or sets a value indicating whether or not Windows argument conventions are allowed....
Definition: GetOpt.cs:149
bool AllowPowershellConventions
Gets or sets a value indicating whether or not Powershell-style arguments are allowed....
Definition: GetOpt.cs:160
const char SingleAtSymbol
A single "at" character. This character is used when AllowParamFiles is enabled, to determine whether...
Definition: GetOpt.cs:68
const char MissingArgChar
The character that is returned when an option is missing a required argument.
Definition: GetOpt.cs:20
bool IgnoreEmptyAppArgs
Gets or sets a value indicating whether or not empty AppArgs are ignored or throw an exception....
Definition: GetOpt.cs:130
const char SingleDash
A single dash character. This is the character that is searched for, when parsing POSIX-/GNU-like opt...
Definition: GetOpt.cs:41
string[] m_appArgs
Definition: GetOpt.cs:86
const char InvalidOptChar
The character that is returned when an invalid option is returned.
Definition: GetOpt.cs:25
void ResetOptPosition()
Resets the option position to 1.
string? ShortOpts
The short opts to use.
Definition: GetOpt.cs:78
int CurrentIndex
Gets the current index of the app arguments being parsed.
Definition: GetOpt.cs:196
const string DoubleDash
This is the string getopt.net looks for when DoubleDashStopsParsing is enabled.
Definition: GetOpt.cs:35
bool IsLongOption(string arg)
Gets a value indicating whether or not the current string is a long option.
Definition: GetOpt.cs:639
bool MustReturnChar1()
Gets a value indicating whether or not non-options should be handled as if they were the argument of ...
int m_optPosition
The current position in a multi-option string such as "-xvzRf" when parsing short options.
Definition: GetOpt.cs:224
GetOpt(string[] appArgs, string shortOpts, params Option[] options)
Specialised constructor.
Definition: GetOpt.cs:210
int GetNextOpt(out string? outOptArg)
Gets the next option in the list.
Definition: GetOpt.cs:272
Generic exception class that is thrown when the parser is not configured to ignore errors.
ArgumentType
Enumeration containing the argument types possible for getopt.
Definition: ArgumentType.cs:8
Represents a single long option for getopt.
Definition: Option.cs:8