From 72937be9b44b6e186abbca8e1afa28505aaaf6be Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 13:49:01 +0300 Subject: [PATCH 01/11] Add new common restrictions --- .../OptionValueRestrictionParser.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs index 4198cb4..9d3e510 100644 --- a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs +++ b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs @@ -170,10 +170,16 @@ private static Predicate ParsePredicate(string[] data) "INRANGE" or "MINMAX" => ParseInRangePredicate(parameters), "ONEOF" or "INLIST" => ParseOneOfPredicate(parameters), "MATCH" or "REGEX" => ParseMatchPredicate(parameters), + "DEFAULT" => ParseDefaultPredicate(parameters), + "NULL" => ParseNullPredicate(parameters), + "NULLOREMPTY" => ParseNullOrEmptyPredicate(parameters), + "NULLORWHITESPACE" => ParseNullOrWhiteSpacePredicate(parameters), + "EMPTY" => ParseEmptyPredicate(parameters), "DIRECTORYEXISTS" or "DIRECTORY" => ParseDirectoryExistsPredicate(parameters), - "FILEEXISTS" or "FILE" => ParseFileExistsPredicate(parameters), + "FILEEXISTS" => ParseFileExistsPredicate(parameters), "MAXFILESIZE" or "MAXSIZE" => ParseMaxFileSizePredicate(parameters), "EXTENSION" or "EXT" => ParseFileExtensionPredicate(parameters), + "FILE" => ParseFilePredicate(parameters), _ => throw new ArgumentOutOfRangeException(nameof(data), "Unknown predicate name") }; @@ -256,6 +262,48 @@ private static Predicate ParseMatchPredicate(string[] parameters) return value => value is not null && regex.IsMatch(value.ToString() ?? string.Empty); } + private static Predicate ParseDefaultPredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length)); + + return value => EqualityComparer.Default.Equals(value, default!); + } + + private static Predicate ParseNullPredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length)); + + return value => value is null; + } + + private static Predicate ParseNullOrEmptyPredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length)); + + return value => value is null || + (value is string text && string.IsNullOrEmpty(text)); + } + + private static Predicate ParseNullOrWhiteSpacePredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length)); + + return value => value is null || + (value is string text && string.IsNullOrWhiteSpace(text)); + } + + private static Predicate ParseEmptyPredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length)); + + return value => value is string text && text.Length == 0; + } + private static Predicate ParseDirectoryExistsPredicate(string[] parameters) { ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); @@ -333,4 +381,20 @@ private static Predicate ParseFileExtensionPredicate(string[] parameters) } }; } + + private static Predicate ParseFilePredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + + if (parameters.Length == 0) + return ParseFileExistsPredicate(parameters); + + Predicate fileExistsPredicate = ParseFileExistsPredicate([]); + Predicate fileExtensionPredicate = ParseFileExtensionPredicate(parameters); + + List> predicates = [fileExistsPredicate, fileExtensionPredicate]; + List connections = [LogicalOperator.And]; + + return CombinePredicates(predicates, connections); + } } From 018e021b3f8d30ea2472733f57acae09723785b2 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 14:54:33 +0300 Subject: [PATCH 02/11] Update documentation --- .../ParserGenerationUsingAttributes.md | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Documentation/ParserGenerationUsingAttributes.md b/Documentation/ParserGenerationUsingAttributes.md index 3605e0d..b1545ca 100644 --- a/Documentation/ParserGenerationUsingAttributes.md +++ b/Documentation/ParserGenerationUsingAttributes.md @@ -191,10 +191,16 @@ The following predicates are available: 7. `inrange` (`minmax`): takes two parameters. The option value must be greater than or equal to the first parameter and less than or equal to the second parameter. The parameter types must be double, and the option value type must have the overloaded `>=` and `<=` operators. 8. `oneof` (`inlist`): takes one or more parameters. The option value, converted to a string, must equal one of the specified parameters. 9. `match` (`regex`): takes a single parameter. The option value, converted to a string, must match the specified parameter representing a regular expression. Anything written after the first space will be avaluated as a regular expression, so it can contain spaces. -10. `directoryexists` (`directory`): takes no parameters. The option value must be a string representing the path to an existing directory. -11. `fileexists` (`file`): takes no parameters. The option value must be a string representing the path to an existing file. -12. `maxfilesize` (`maxsize`): takes a single parameter. The option value must be a string representing the path to a file whose size is less than or equal to this parameter (in bytes). -13. `extension` (`ext`): takes one or more parameters. The option value must be a string representing the path to a file whose extension matches one of the specified parameters. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter. +10. `default`: takes no parameters. The option value must be the default for corresponding type. +11. `null`: takes no parameters. The option value must be null. +12. `nullorempty`: takes no parameters. The option value must be null or empty string. +13. `nullorwhitespace`: takes no parameters. The option value must be null, an empty string, or a string consisting only of whitespace characters. +14. `empty`: takes no parameters. The option value must be an empty string. +15. `directoryexists` (`directory`): takes no parameters. The option value must be a string representing the path to an existing directory. +16. `fileexists`: takes no parameters. The option value must be a string representing the path to an existing file. +17. `maxfilesize` (`maxsize`): takes a single parameter. The option value must be a string representing the path to a file whose size is less than or equal to this parameter (in bytes). +18. `extension` (`ext`): takes one or more parameters. The option value must be a string representing the path to a file whose extension matches one of the specified parameters. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter. +19. `file`: takes zero or more parameters. The option value must be a string representing the path to an existing file whose extension matches one of the specified parameters. If no parameters are provided, only the file's existence is checked. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter. Examples of simple restrictions are provided below: 1. `== 5`: the option value must be equal to 5. @@ -206,10 +212,16 @@ Examples of simple restrictions are provided below: 7. `inrange 0 5`: the option value must be within the range from 0 to 5. 8. `oneof 1 3 6`: the option value must be one of the values: 1, 3 or 6. 9. `match ^[A-Z][a-z]*$`: the option value must match the regular expression `^[A-Z][a-z]*$`. -10. `directoryexists`: the option value must be a string representing the path to an existing directory. -11. `fileexists`: the option value must be a string representing the path to an existing file. -12. `maxfilesize 10240`: the option value must be a string representing the path to a file whose size is less than or equal to 10240 bytes. -13. `extension jpg png`: the option value must be a string representing the path to a file whose extension matches either `jpg` or `png`. +10. `default`: the option value must be the default for corresponding type. +11. `null`: the option value must be null. +12. `nullorempty`: the option value must be null or empty string. +13. `nullorwhitespace`: the option value must be null, an empty string, or a string consisting only of whitespace characters. +14. `empty`: the option value must be an empty string. +15. `directoryexists`: the option value must be a string representing the path to an existing directory. +16. `fileexists`: the option value must be a string representing the path to an existing file. +17. `maxfilesize 10240`: the option value must be a string representing the path to a file whose size is less than or equal to 10240 bytes. +18. `extension jpg png`: the option value must be a string representing the path to a file whose extension matches either `jpg` or `png`. +19. `file mp4 mkv`: the option value must be a string representing the path to an existing file whose extension matches either `mp4` or `mkv`. Examples of complex restrictions are provided below: 1. `< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200`. From c8fb9ccf4160c72d8eb7030919dc2450f12c288d Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 14:54:49 +0300 Subject: [PATCH 03/11] Update tests --- ...onValueRestrictionParserGeneratorConfig.cs | 72 +++++++++++++- .../ParserGeneratorTests.cs | 98 +++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs b/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs index 6bd2bd5..55d44a7 100644 --- a/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs +++ b/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs @@ -41,6 +41,24 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public const string PhoneLongName = "Phone"; public const string? PhoneValueRestriction = $"match {PhonePattern}"; + public const string DefaultValueTypeLongName = "DefaultValueType"; + public const string? DefaultValueTypeValueRestriction = "default"; + + public const string DefaultReferenceTypeLongName = "DefaultReferenceType"; + public const string? DefaultReferenceTypeValueRestriction = "default"; + + public const string NullStringLongName = "NullString"; + public const string? NullStringValueRestriction = "null"; + + public const string NullOrEmptyStringLongName = "NullOrEmptyString"; + public const string? NullOrEmptyStringValueRestriction = "nullorempty"; + + public const string NullOrWhiteSpaceStringLongName = "NullOrWhiteSpaceString"; + public const string? NullOrWhiteSpaceStringValueRestriction = "nullorwhitespace"; + + public const string EmptyStringLongName = "EmptyString"; + public const string? EmptyStringValueRestriction = "empty"; + public const string OutputFilePathLongName = "OutputFilePath"; public const string? OutputFilePathValueRestriction = "extension jpg .png GiF"; @@ -50,7 +68,6 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public const string NumbersLongName = "Numbers"; public const string? NumbersValueRestriction = "< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200"; -#pragma warning disable SYSLIB1045 // Use GeneratedRegexAttribute to generate the regular expression implementation at compile time public static Predicate AngleValueRestrictionPredicate { get; } = t => t == 45; public static Predicate WeightValueRestrictionPredicate { get; } = t => t != 100; public static Predicate AgeValueRestrictionPredicate { get; } = t => t < 20; @@ -58,10 +75,27 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public static Predicate HeightValueRestrictionPredicate { get; } = t => t > 22; public static Predicate LengthValueRestrictionPredicate { get; } = t => t >= 23; public static Predicate VerbosityValueRestrictionPredicate { get; } = t => t >= 0 && t <= 4; + +#pragma warning disable SYSLIB1045 // Use GeneratedRegexAttribute to generate the regular expression implementation at compile time public static Predicate NameValueRestrictionPredicate { get; } = t => new Regex(NamePattern).IsMatch(t); public static Predicate PhoneValueRestrictionPredicate { get; } = t => new Regex(PhonePattern).IsMatch(t); #pragma warning restore SYSLIB1045 // Use GeneratedRegexAttribute to generate the regular expression implementation at compile time + public static Predicate DefaultValueTypeValueRestrictionPredicate { get; } = t => + { + return EqualityComparer.Default.Equals(t, default); + }; + + public static Predicate DefaultReferenceTypeValueRestrictionPredicate { get; } = t => + { + return EqualityComparer.Default.Equals(t, default); + }; + + public static Predicate NullStringValueRestrictionPredicate { get; } = t => t is null; + public static Predicate NullOrEmptyStringValueRestrictionPredicate { get; } = string.IsNullOrEmpty; + public static Predicate NullOrWhiteSpaceStringValueRestrictionPredicate { get; } = string.IsNullOrWhiteSpace; + public static Predicate EmptyStringValueRestrictionPredicate { get; } = t => t is not null && t.Length == 0; + public static Predicate OutputFilePathValueRestrictionPredicate { get; } = t => { var allowedExtensions = new List { ".JPG", ".PNG", ".GIF" }; @@ -148,6 +182,42 @@ internal partial class OptionValueRestrictionParserGeneratorConfig ] public string? Phone { get; set; } + [ValueOption( + DefaultValueTypeLongName, + valueRestriction: DefaultValueTypeValueRestriction) + ] + public Point DefaultValueType { get; set; } + + [ValueOption( + DefaultReferenceTypeLongName, + valueRestriction: DefaultReferenceTypeValueRestriction) + ] + public string? DefaultReferenceType { get; set; } + + [ValueOption( + NullStringLongName, + valueRestriction: NullStringValueRestriction) + ] + public string? NullString { get; set; } + + [ValueOption( + NullOrEmptyStringLongName, + valueRestriction: NullOrEmptyStringValueRestriction) + ] + public string? NullOrEmptyString { get; set; } + + [ValueOption( + NullOrWhiteSpaceStringLongName, + valueRestriction: NullOrWhiteSpaceStringValueRestriction) + ] + public string? NullOrWhiteSpaceString { get; set; } + + [ValueOption( + EmptyStringLongName, + valueRestriction: EmptyStringValueRestriction) + ] + public string? EmptyString { get; set; } + [ValueOption( OutputFilePathLongName, valueRestriction: OutputFilePathValueRestriction) diff --git a/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs b/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs index c084b58..3b26146 100644 --- a/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs +++ b/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs @@ -492,6 +492,36 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre false, out IValueOption? phoneOption); + bool defaultValueTypeOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.DefaultValueTypeLongName, + false, + out IValueOption? defaultValueTypeOption); + + bool defaultReferenceTypeOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.DefaultReferenceTypeLongName, + false, + out IValueOption? defaultReferenceTypeOption); + + bool nullStringOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.NullStringLongName, + false, + out IValueOption? nullStringOption); + + bool nullOrEmptyStringOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.NullOrEmptyStringLongName, + false, + out IValueOption? nullOrEmptyStringOption); + + bool nullOrWhiteSpaceStringOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.NullOrWhiteSpaceStringLongName, + false, + out IValueOption? nullOrWhiteSpaceStringOption); + + bool emptyStringOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.EmptyStringLongName, + false, + out IValueOption? emptyStringOption); + bool outputFilePathOptionFound = argumentParser.FindFirstValueOptionByLongName( OptionValueRestrictionParserGeneratorConfig.OutputFilePathLongName, false, @@ -516,6 +546,12 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.True(verbosityOptionFound); Assert.True(nameOptionFound); Assert.True(phoneOptionFound); + Assert.True(defaultValueTypeOptionFound); + Assert.True(defaultReferenceTypeOptionFound); + Assert.True(nullStringOptionFound); + Assert.True(nullOrEmptyStringOptionFound); + Assert.True(nullOrWhiteSpaceStringOptionFound); + Assert.True(emptyStringOptionFound); Assert.True(outputFilePathOptionFound); Assert.True(modeOptionFound); Assert.True(numbersOptionFound); @@ -529,6 +565,12 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.NotNull(verbosityOption); Assert.NotNull(nameOption); Assert.NotNull(phoneOption); + Assert.NotNull(defaultValueTypeOption); + Assert.NotNull(defaultReferenceTypeOption); + Assert.NotNull(nullStringOption); + Assert.NotNull(nullOrEmptyStringOption); + Assert.NotNull(nullOrWhiteSpaceStringOption); + Assert.NotNull(emptyStringOption); Assert.NotNull(outputFilePathOption); Assert.NotNull(modeOption); Assert.NotNull(numbersOption); @@ -542,6 +584,12 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.NotNull(verbosityOption.ValueRestriction); Assert.NotNull(nameOption.ValueRestriction); Assert.NotNull(phoneOption.ValueRestriction); + Assert.NotNull(defaultValueTypeOption.ValueRestriction); + Assert.NotNull(defaultReferenceTypeOption.ValueRestriction); + Assert.NotNull(nullStringOption.ValueRestriction); + Assert.NotNull(nullOrEmptyStringOption.ValueRestriction); + Assert.NotNull(nullOrWhiteSpaceStringOption.ValueRestriction); + Assert.NotNull(emptyStringOption.ValueRestriction); Assert.NotNull(outputFilePathOption.ValueRestriction); Assert.NotNull(modeOption.ValueRestriction); Assert.NotNull(numbersOption.ValueRestriction); @@ -644,6 +692,56 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre phoneOption.ValueRestriction.IsValueAllowed.Invoke(phone)); } + for (int x = -10; x <= 10; ++x) + { + for (int y = -10; y <= 10; ++y) + { + var point = new Point(x, y); + + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig + .DefaultValueTypeValueRestrictionPredicate.Invoke(point), + defaultValueTypeOption.ValueRestriction.IsValueAllowed.Invoke(point)); + } + } + + string?[] texts = + [ + null, + default, + string.Empty, + "+1 123 345 67 89", + "31233456789", + "-a bbbb cDe45 67 8D", + ".", + " ", + " ", + " a " + ]; + + foreach (string? text in texts) + { + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.DefaultReferenceTypeValueRestrictionPredicate.Invoke(text), + defaultReferenceTypeOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.NullStringValueRestrictionPredicate.Invoke(text), + nullStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.NullOrEmptyStringValueRestrictionPredicate.Invoke(text), + nullOrEmptyStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.NullOrWhiteSpaceStringValueRestrictionPredicate.Invoke(text), + nullOrWhiteSpaceStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.EmptyStringValueRestrictionPredicate.Invoke(text), + emptyStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + } + string[] outputFilePaths = [ "/home/user/text.txt", From ef86186087b836850d879d2dca7ebd99204ab985 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:36:14 +0300 Subject: [PATCH 04/11] Add support of predicate negation --- .../MultipleValueOptionAttribute.cs | 2 +- .../Attributes/ValueOptionAttribute.cs | 2 +- .../OptionValueRestrictionParser.cs | 140 +++++++++++------- 3 files changed, 85 insertions(+), 59 deletions(-) diff --git a/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs b/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs index fce7283..b8e2e1b 100644 --- a/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs +++ b/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs @@ -147,7 +147,7 @@ public override ICommonOption CreateOption(object source, PropertyInfo propertyI private static OptionValueRestriction>? CreateValueRestriction(string? data) { return data is not null && !string.IsNullOrWhiteSpace(data) - ? OptionValueRestrictionParser.ParseForList(data, true) + ? new OptionValueRestrictionParser().ParseForList(data, true) : null; } } diff --git a/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs b/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs index 9c9816e..c48413d 100644 --- a/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs +++ b/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs @@ -172,7 +172,7 @@ public override ICommonOption CreateOption(object source, PropertyInfo propertyI private static OptionValueRestriction? CreateValueRestriction(string? data) { return data is not null && !string.IsNullOrWhiteSpace(data) - ? OptionValueRestrictionParser.Parse(data, true) + ? new OptionValueRestrictionParser().Parse(data, true) : null; } diff --git a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs index 9d3e510..281764d 100644 --- a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs +++ b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs @@ -8,12 +8,21 @@ namespace NetArgumentParser.Options.Configuration; -public static class OptionValueRestrictionParser +public class OptionValueRestrictionParser { public const string ExpectedFormat = "f1 p1 ...\\nOP f2 p1 ...\\n ...\\n?msg"; - public const char PartsSeparator = '\n'; - public const char SubpartsSeparator = ' '; - public const char MessageIndicator = '?'; + + public OptionValueRestrictionParser( + char partsSeparator = '\n', + char subpartsSeparator = ' ', + char negationIndicator = '!', + char messageIndicator = '?') + { + PartsSeparator = partsSeparator; + SubpartsSeparator = subpartsSeparator; + NegationIndicator = negationIndicator; + MessageIndicator = messageIndicator; + } private enum LogicalOperator { @@ -21,7 +30,12 @@ private enum LogicalOperator Or } - public static OptionValueRestriction Parse(string data, bool makePredicatesSafe = false) + public char PartsSeparator { get; } + public char SubpartsSeparator { get; } + public char NegationIndicator { get; } + public char MessageIndicator { get; } + + public OptionValueRestriction Parse(string data, bool makePredicatesSafe = false) { ExtendedArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); @@ -69,7 +83,7 @@ public static OptionValueRestriction Parse(string data, bool makePredicate return new OptionValueRestriction(isValueAllowed, valueNotSatisfyRestrictionMessage); } - public static OptionValueRestriction> ParseForList(string data, bool makePredicatesSafe = false) + public OptionValueRestriction> ParseForList(string data, bool makePredicatesSafe = false) { ExtendedArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); @@ -97,6 +111,12 @@ private static Predicate MakeSafePredicate(Predicate predicate) }; } + private static Predicate NegatePredicate(Predicate predicate) + { + ExtendedArgumentNullException.ThrowIfNull(predicate, nameof(predicate)); + return t => !predicate(t); + } + private static Predicate CombinePredicates(List> predicates, List connections) { ExtendedArgumentNullException.ThrowIfNull(predicates, nameof(predicates)); @@ -145,46 +165,6 @@ private static bool TryParseLogicalOperator(string data, out LogicalOperator log return result is not null; } - private static Predicate ParsePredicate(string[] data) - { - ExtendedArgumentNullException.ThrowIfNull(data, nameof(data)); - - if (data.Length == 0) - { - throw new ArgumentException( - $"Recieved data has incorrect format. Expected: {ExpectedFormat}", - nameof(data)); - } - - string name = data[0]; - string[] parameters = [.. data.Skip(1)]; - - return name.ToUpper(CultureInfo.CurrentCulture) switch - { - "EQUAL" or "==" or "=" => ParseEqualPredicate(parameters), - "NOTEQUAL" or "!=" or "<>" => ParseNotEqualPredicate(parameters), - "LESS" or "<" => ParseLessPredicate(parameters), - "LESSOREQUAL" or "<=" => ParseLessOrEqualPredicate(parameters), - "GREATER" or ">" => ParseGreaterPredicate(parameters), - "GREATEROREQUAL" or ">=" => ParseGreaterOrEqualPredicate(parameters), - "INRANGE" or "MINMAX" => ParseInRangePredicate(parameters), - "ONEOF" or "INLIST" => ParseOneOfPredicate(parameters), - "MATCH" or "REGEX" => ParseMatchPredicate(parameters), - "DEFAULT" => ParseDefaultPredicate(parameters), - "NULL" => ParseNullPredicate(parameters), - "NULLOREMPTY" => ParseNullOrEmptyPredicate(parameters), - "NULLORWHITESPACE" => ParseNullOrWhiteSpacePredicate(parameters), - "EMPTY" => ParseEmptyPredicate(parameters), - "DIRECTORYEXISTS" or "DIRECTORY" => ParseDirectoryExistsPredicate(parameters), - "FILEEXISTS" => ParseFileExistsPredicate(parameters), - "MAXFILESIZE" or "MAXSIZE" => ParseMaxFileSizePredicate(parameters), - "EXTENSION" or "EXT" => ParseFileExtensionPredicate(parameters), - "FILE" => ParseFilePredicate(parameters), - - _ => throw new ArgumentOutOfRangeException(nameof(data), "Unknown predicate name") - }; - } - private static Predicate ParseComparePredicate( string[] parameters, Func compareFunc) @@ -251,17 +231,6 @@ private static Predicate ParseOneOfPredicate(string[] parameters) return value => parameters.Contains(value?.ToString() ?? string.Empty); } - private static Predicate ParseMatchPredicate(string[] parameters) - { - ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); - DefaultExceptions.ThrowIfEqual(parameters.Length, 0, nameof(parameters.Length)); - - string pattern = string.Join($"{SubpartsSeparator}", parameters); - - var regex = new Regex(pattern); - return value => value is not null && regex.IsMatch(value.ToString() ?? string.Empty); - } - private static Predicate ParseDefaultPredicate(string[] parameters) { ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); @@ -397,4 +366,61 @@ private static Predicate ParseFilePredicate(string[] parameters) return CombinePredicates(predicates, connections); } + + private Predicate ParseMatchPredicate(string[] parameters) + { + ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters)); + DefaultExceptions.ThrowIfEqual(parameters.Length, 0, nameof(parameters.Length)); + + string pattern = string.Join($"{SubpartsSeparator}", parameters); + + var regex = new Regex(pattern); + return value => value is not null && regex.IsMatch(value.ToString() ?? string.Empty); + } + + private Predicate ParsePredicate(string[] data) + { + ExtendedArgumentNullException.ThrowIfNull(data, nameof(data)); + + if (data.Length == 0) + { + throw new ArgumentException( + $"Recieved data has incorrect format. Expected: {ExpectedFormat}", + nameof(data)); + } + + bool shouldNegatePredicate = data.Length > 0 && data[0].StartsWith(NegationIndicator); + + string name = shouldNegatePredicate ? data[0].Substring(1) : data[0]; + string[] parameters = [.. data.Skip(1)]; + + Predicate predicate = name.ToUpper(CultureInfo.CurrentCulture) switch + { + "EQUAL" or "==" or "=" => ParseEqualPredicate(parameters), + "NOTEQUAL" or "!=" or "<>" => ParseNotEqualPredicate(parameters), + "LESS" or "<" => ParseLessPredicate(parameters), + "LESSOREQUAL" or "<=" => ParseLessOrEqualPredicate(parameters), + "GREATER" or ">" => ParseGreaterPredicate(parameters), + "GREATEROREQUAL" or ">=" => ParseGreaterOrEqualPredicate(parameters), + "INRANGE" or "MINMAX" => ParseInRangePredicate(parameters), + "ONEOF" or "INLIST" => ParseOneOfPredicate(parameters), + "MATCH" or "REGEX" => ParseMatchPredicate(parameters), + "DEFAULT" => ParseDefaultPredicate(parameters), + "NULL" => ParseNullPredicate(parameters), + "NULLOREMPTY" => ParseNullOrEmptyPredicate(parameters), + "NULLORWHITESPACE" => ParseNullOrWhiteSpacePredicate(parameters), + "EMPTY" => ParseEmptyPredicate(parameters), + "DIRECTORYEXISTS" or "DIRECTORY" => ParseDirectoryExistsPredicate(parameters), + "FILEEXISTS" => ParseFileExistsPredicate(parameters), + "MAXFILESIZE" or "MAXSIZE" => ParseMaxFileSizePredicate(parameters), + "EXTENSION" or "EXT" => ParseFileExtensionPredicate(parameters), + "FILE" => ParseFilePredicate(parameters), + + _ => throw new ArgumentOutOfRangeException(nameof(data), "Unknown predicate name") + }; + + return shouldNegatePredicate + ? NegatePredicate(predicate) + : predicate; + } } From 7963bd932904c7c5af7597edaa7c8772df9abcb4 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:36:30 +0300 Subject: [PATCH 05/11] Update documentation --- Documentation/ParserGenerationUsingAttributes.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Documentation/ParserGenerationUsingAttributes.md b/Documentation/ParserGenerationUsingAttributes.md index b1545ca..fcd4f90 100644 --- a/Documentation/ParserGenerationUsingAttributes.md +++ b/Documentation/ParserGenerationUsingAttributes.md @@ -179,6 +179,8 @@ logical_operator predicateK_name parameter1 ... parameterM In other words, a string consists of one or more predicates, each separated by a newline character `\n`. Following the predicate name, its parameters are listed. A logical connective (`AND` or `OR`) may precede the predicate name; if omitted, it defaults to `AND`. A line that begins with the `?` character specifies a message to be displayed if the option value doesn't satisfy the restriction. However, this message can be omitted. +In addition, you can negate predicate. To do this, place `!` before the predicate name. + It is also important to note that parentheses are not supported, and logical connectives will be evaluated in the order in which they are specified. Thus, `x OR y AND z` will actually be interpreted as `(x OR y) AND z`. Additionally, logical connectives have aliases: for `OR`, they are `||` and `|`, while for `AND`, they are `&&` and `&`. The following predicates are available: @@ -210,11 +212,11 @@ Examples of simple restrictions are provided below: 5. `> 5`: the option value must be greater than 5. 6. `>= 5`: the option value must be greater than or equal to 5. 7. `inrange 0 5`: the option value must be within the range from 0 to 5. -8. `oneof 1 3 6`: the option value must be one of the values: 1, 3 or 6. +8. `oneof 1 3 6`: the option value must be one of: 1, 3 or 6. 9. `match ^[A-Z][a-z]*$`: the option value must match the regular expression `^[A-Z][a-z]*$`. 10. `default`: the option value must be the default for corresponding type. 11. `null`: the option value must be null. -12. `nullorempty`: the option value must be null or empty string. +12. `nullorempty`: the option value must be null or an empty string. 13. `nullorwhitespace`: the option value must be null, an empty string, or a string consisting only of whitespace characters. 14. `empty`: the option value must be an empty string. 15. `directoryexists`: the option value must be a string representing the path to an existing directory. @@ -223,8 +225,12 @@ Examples of simple restrictions are provided below: 18. `extension jpg png`: the option value must be a string representing the path to a file whose extension matches either `jpg` or `png`. 19. `file mp4 mkv`: the option value must be a string representing the path to an existing file whose extension matches either `mp4` or `mkv`. +Examples of restrictions with negation are provided below: +1. `!oneof 1 3 6`: the option value mustn't be one of: 1, 3 or 6. +2. `!nullorempty`: the option value mustn't be null or an empty string. + Examples of complex restrictions are provided below: -1. `< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200`. +1. `< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200\nAND !oneof 77 -77 88`. 2. `fileexists\n&& extension jpg png\n?file must exists and be an image`. Finally, here is an example of creating an option with a restriction using an attribute: From 5c484152ed1322c5e81c2df6607afccdac1682a6 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:36:41 +0300 Subject: [PATCH 06/11] Update tests --- ...OptionValueRestrictionParserGeneratorConfig.cs | 15 +++++++++++++-- .../ParserGeneratorTests.cs | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs b/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs index 55d44a7..c6b1c66 100644 --- a/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs +++ b/Tests/NetArgumentParser.Tests/Models/Configurations/OptionValueRestrictionParserGeneratorConfig.cs @@ -56,6 +56,9 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public const string NullOrWhiteSpaceStringLongName = "NullOrWhiteSpaceString"; public const string? NullOrWhiteSpaceStringValueRestriction = "nullorwhitespace"; + public const string NotNullOrWhiteSpaceStringLongName = "NotNullOrWhiteSpaceString"; + public const string? NotNullOrWhiteSpaceStringValueRestriction = "!nullorwhitespace"; + public const string EmptyStringLongName = "EmptyString"; public const string? EmptyStringValueRestriction = "empty"; @@ -66,7 +69,7 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public const string? ModeValueRestriction = $"oneof {nameof(FileMode.Append)} {nameof(FileMode.Open)}"; public const string NumbersLongName = "Numbers"; - public const string? NumbersValueRestriction = "< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200"; + public const string? NumbersValueRestriction = "< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200\nAND !oneof 77 -77 88"; public static Predicate AngleValueRestrictionPredicate { get; } = t => t == 45; public static Predicate WeightValueRestrictionPredicate { get; } = t => t != 100; @@ -94,6 +97,7 @@ internal partial class OptionValueRestrictionParserGeneratorConfig public static Predicate NullStringValueRestrictionPredicate { get; } = t => t is null; public static Predicate NullOrEmptyStringValueRestrictionPredicate { get; } = string.IsNullOrEmpty; public static Predicate NullOrWhiteSpaceStringValueRestrictionPredicate { get; } = string.IsNullOrWhiteSpace; + public static Predicate NotNullOrWhiteSpaceStringValueRestrictionPredicate { get; } = t => !string.IsNullOrWhiteSpace(t); public static Predicate EmptyStringValueRestrictionPredicate { get; } = t => t is not null && t.Length == 0; public static Predicate OutputFilePathValueRestrictionPredicate { get; } = t => @@ -123,7 +127,8 @@ internal partial class OptionValueRestrictionParserGeneratorConfig static bool Greater(Number t) => t > 100; static bool OneOf(Number t) => new List() { 1, 5, 7, 10 }.Any(x => t == x); static bool InRange(Number t) => t >= -200 && t <= 200; - static bool CombinedPredicate(Number t) => (Less(t) || Greater(t) || OneOf(t)) && InRange(t); + static bool NotOneOf(Number t) => !new List() { 77, -77, 88 }.Any(x => t == x); + static bool CombinedPredicate(Number t) => (Less(t) || Greater(t) || OneOf(t)) && InRange(t) && NotOneOf(t); return t.All(CombinedPredicate); }; @@ -212,6 +217,12 @@ internal partial class OptionValueRestrictionParserGeneratorConfig ] public string? NullOrWhiteSpaceString { get; set; } + [ValueOption( + NotNullOrWhiteSpaceStringLongName, + valueRestriction: NotNullOrWhiteSpaceStringValueRestriction) + ] + public string? NotNullOrWhiteSpaceString { get; set; } + [ValueOption( EmptyStringLongName, valueRestriction: EmptyStringValueRestriction) diff --git a/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs b/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs index 3b26146..a61edee 100644 --- a/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs +++ b/Tests/NetArgumentParser.Tests/ParserGeneratorTests.cs @@ -517,6 +517,11 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre false, out IValueOption? nullOrWhiteSpaceStringOption); + bool notNullOrWhiteSpaceStringOptionFound = argumentParser.FindFirstValueOptionByLongName( + OptionValueRestrictionParserGeneratorConfig.NotNullOrWhiteSpaceStringLongName, + false, + out IValueOption? notNullOrWhiteSpaceStringOption); + bool emptyStringOptionFound = argumentParser.FindFirstValueOptionByLongName( OptionValueRestrictionParserGeneratorConfig.EmptyStringLongName, false, @@ -551,6 +556,7 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.True(nullStringOptionFound); Assert.True(nullOrEmptyStringOptionFound); Assert.True(nullOrWhiteSpaceStringOptionFound); + Assert.True(notNullOrWhiteSpaceStringOptionFound); Assert.True(emptyStringOptionFound); Assert.True(outputFilePathOptionFound); Assert.True(modeOptionFound); @@ -570,6 +576,7 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.NotNull(nullStringOption); Assert.NotNull(nullOrEmptyStringOption); Assert.NotNull(nullOrWhiteSpaceStringOption); + Assert.NotNull(notNullOrWhiteSpaceStringOption); Assert.NotNull(emptyStringOption); Assert.NotNull(outputFilePathOption); Assert.NotNull(modeOption); @@ -589,6 +596,7 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre Assert.NotNull(nullStringOption.ValueRestriction); Assert.NotNull(nullOrEmptyStringOption.ValueRestriction); Assert.NotNull(nullOrWhiteSpaceStringOption.ValueRestriction); + Assert.NotNull(notNullOrWhiteSpaceStringOption.ValueRestriction); Assert.NotNull(emptyStringOption.ValueRestriction); Assert.NotNull(outputFilePathOption.ValueRestriction); Assert.NotNull(modeOption.ValueRestriction); @@ -737,6 +745,10 @@ public void ConfigureParser_AllValueOptionTypes_ValueRestrictionsConfiguredCorre OptionValueRestrictionParserGeneratorConfig.NullOrWhiteSpaceStringValueRestrictionPredicate.Invoke(text), nullOrWhiteSpaceStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + Assert.Equal( + OptionValueRestrictionParserGeneratorConfig.NotNullOrWhiteSpaceStringValueRestrictionPredicate.Invoke(text), + notNullOrWhiteSpaceStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); + Assert.Equal( OptionValueRestrictionParserGeneratorConfig.EmptyStringValueRestrictionPredicate.Invoke(text), emptyStringOption.ValueRestriction.IsValueAllowed.Invoke(text!)); From f99d19bc4d1c05403ca9b50aea517c25d9000b3e Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:39:18 +0300 Subject: [PATCH 07/11] Add setters to `IValueOption` --- Core/NetArgumentParser/Options/IValueOption.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/NetArgumentParser/Options/IValueOption.cs b/Core/NetArgumentParser/Options/IValueOption.cs index 1147bdd..566f537 100644 --- a/Core/NetArgumentParser/Options/IValueOption.cs +++ b/Core/NetArgumentParser/Options/IValueOption.cs @@ -13,8 +13,8 @@ public interface IValueOption : ICommonOption, IValueOptionDescriptionDesigne string MetaVariable { get; } IReadOnlyCollection Choices { get; } - DefaultOptionValue? DefaultValue { get; } - OptionValueRestriction? ValueRestriction { get; } + DefaultOptionValue? DefaultValue { get; set; } + OptionValueRestriction? ValueRestriction { get; set; } IValueConverter? Converter { get; set; } void HandleDefaultValue(); From 63691b610b0c208007a814e2c6d9b5c825dda359 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:39:31 +0300 Subject: [PATCH 08/11] Update TODO.md --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 81ee97e..ae16278 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,7 @@ This is the TODO file where you can find some features and content that need to ## Features - [ ] Come up with new features. +- [ ] Add support of `--`. - [ ] Add mutually inclusive groups. - [ ] Add the ability to sort options (by name, by weight, etc) in application description generator. - [ ] Add XML documentation. From 2657c6b90f8b4b819b6cbfe599503ecc49d98ffc Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:51:18 +0300 Subject: [PATCH 09/11] Add new aliases for common restrictions --- .../Options/Configuration/OptionValueRestrictionParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs index 281764d..eaed4f0 100644 --- a/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs +++ b/Core/NetArgumentParser/Options/Configuration/OptionValueRestrictionParser.cs @@ -399,9 +399,9 @@ private Predicate ParsePredicate(string[] data) "EQUAL" or "==" or "=" => ParseEqualPredicate(parameters), "NOTEQUAL" or "!=" or "<>" => ParseNotEqualPredicate(parameters), "LESS" or "<" => ParseLessPredicate(parameters), - "LESSOREQUAL" or "<=" => ParseLessOrEqualPredicate(parameters), + "LESSOREQUAL" or "MAX" or "<=" => ParseLessOrEqualPredicate(parameters), "GREATER" or ">" => ParseGreaterPredicate(parameters), - "GREATEROREQUAL" or ">=" => ParseGreaterOrEqualPredicate(parameters), + "GREATEROREQUAL" or "MIN" or ">=" => ParseGreaterOrEqualPredicate(parameters), "INRANGE" or "MINMAX" => ParseInRangePredicate(parameters), "ONEOF" or "INLIST" => ParseOneOfPredicate(parameters), "MATCH" or "REGEX" => ParseMatchPredicate(parameters), From ba3df83a403454bcd0489a1132858c7f8c9fb720 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 15:51:29 +0300 Subject: [PATCH 10/11] Update documentation --- Documentation/ParserGenerationUsingAttributes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/ParserGenerationUsingAttributes.md b/Documentation/ParserGenerationUsingAttributes.md index fcd4f90..58e5656 100644 --- a/Documentation/ParserGenerationUsingAttributes.md +++ b/Documentation/ParserGenerationUsingAttributes.md @@ -187,9 +187,9 @@ The following predicates are available: 1. `equal` (`==`, `=`): takes a single parameter. The option value must equal this parameter. The parameter type must be double, and the option value type must have the overloaded `==` operator. 2. `notequal` (`!=`, `<>`): takes a single parameter. The option value must differ from this parameter. The parameter type must be double, and the option value type must have the overloaded `!=` operator. 3. `less` (`<`): takes a single parameter. The option value must be less than this parameter. The parameter type must be double, and the option value type must have the overloaded `<` operator. -4. `lessorequal` (`<=`): takes a single parameter. The option value must be less than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `<=` operator. +4. `lessorequal` (`max`, `<=`): takes a single parameter. The option value must be less than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `<=` operator. 5. `greater` (`>`): takes a single parameter. The option value must be greater than this parameter. The parameter type must be double, and the option value type must have the overloaded `>` operator. -6. `greaterorequal` (`>=`): takes a single parameter. The option value must be greater than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `>=` operator. +6. `greaterorequal` (`min`, `>=`): takes a single parameter. The option value must be greater than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `>=` operator. 7. `inrange` (`minmax`): takes two parameters. The option value must be greater than or equal to the first parameter and less than or equal to the second parameter. The parameter types must be double, and the option value type must have the overloaded `>=` and `<=` operators. 8. `oneof` (`inlist`): takes one or more parameters. The option value, converted to a string, must equal one of the specified parameters. 9. `match` (`regex`): takes a single parameter. The option value, converted to a string, must match the specified parameter representing a regular expression. Anything written after the first space will be avaluated as a regular expression, so it can contain spaces. From b077f5fdf9557ddbc77f1106c2d447fd675f77c9 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Thu, 19 Mar 2026 17:28:39 +0300 Subject: [PATCH 11/11] Update version --- Core/NetArgumentParser/NetArgumentParser.csproj | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/NetArgumentParser/NetArgumentParser.csproj b/Core/NetArgumentParser/NetArgumentParser.csproj index ddc0010..9897e8d 100644 --- a/Core/NetArgumentParser/NetArgumentParser.csproj +++ b/Core/NetArgumentParser/NetArgumentParser.csproj @@ -2,7 +2,7 @@ NetArgumentParser - 1.0.6 + 1.0.7 NetArgumentParser NetArgumentParser yakovypg diff --git a/README.md b/README.md index ed97201..809fe7e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ license - version + version csharp