From 4647bb27397609d10d0413c745003af16daffb86 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Mon, 23 Feb 2026 19:39:53 +0300 Subject: [PATCH 1/3] Add the ability to add default value to description --- .../Attributes/EnumValueOptionAttribute.cs | 12 +++-- .../MultipleValueOptionAttribute.cs | 12 +++-- .../Attributes/ValueOptionAttribute.cs | 20 ++++++-- ...ValueAlreadyAddedToDescriptionException.cs | 29 ++++++++++++ Core/NetArgumentParser/Options/ValueOption.cs | 46 +++++++++++++++++++ 5 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 Core/NetArgumentParser/Options/DefaultValueAlreadyAddedToDescriptionException.cs diff --git a/Core/NetArgumentParser/Attributes/EnumValueOptionAttribute.cs b/Core/NetArgumentParser/Attributes/EnumValueOptionAttribute.cs index 8339bf5..f73b633 100644 --- a/Core/NetArgumentParser/Attributes/EnumValueOptionAttribute.cs +++ b/Core/NetArgumentParser/Attributes/EnumValueOptionAttribute.cs @@ -30,7 +30,8 @@ public EnumValueOptionAttribute( T[]? choices = null, string[]? beforeParseChoices = null, bool addChoicesToDescription = false, - bool addBeforeParseChoicesToDescription = false) + bool addBeforeParseChoicesToDescription = false, + bool addDefaultValueToDescription = false) : base( defaultValue, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -45,7 +46,8 @@ public EnumValueOptionAttribute( choices, beforeParseChoices, addChoicesToDescription, - addBeforeParseChoicesToDescription) + addBeforeParseChoicesToDescription, + addDefaultValueToDescription) { UseDefaultChoices = useDefaultChoices; } @@ -65,7 +67,8 @@ public EnumValueOptionAttribute( T[]? choices = null, string[]? beforeParseChoices = null, bool addChoicesToDescription = false, - bool addBeforeParseChoicesToDescription = false) + bool addBeforeParseChoicesToDescription = false, + bool addDefaultValueToDescription = false) : base( choices, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -79,7 +82,8 @@ public EnumValueOptionAttribute( aliases, beforeParseChoices, addChoicesToDescription, - addBeforeParseChoicesToDescription) + addBeforeParseChoicesToDescription, + addDefaultValueToDescription) { UseDefaultChoices = useDefaultChoices; } diff --git a/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs b/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs index fa4a08a..b816bd8 100644 --- a/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs +++ b/Core/NetArgumentParser/Attributes/MultipleValueOptionAttribute.cs @@ -28,7 +28,8 @@ public MultipleValueOptionAttribute( bool ignoreOrderInChoices = false, string[]? aliases = null, ContextCaptureType contextCaptureType = ContextCaptureType.None, - int numberOfItemsToCapture = -1) + int numberOfItemsToCapture = -1, + bool addDefaultValueToDescription = false) : base( defaultValue, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -41,7 +42,8 @@ public MultipleValueOptionAttribute( ignoreCaseInChoices, aliases, choices: null, - beforeParseChoices: null) + beforeParseChoices: null, + addDefaultValueToDescription: addDefaultValueToDescription) { IgnoreOrderInChoices = ignoreOrderInChoices; ContextCapture = CreateContextCapture(contextCaptureType, numberOfItemsToCapture); @@ -59,7 +61,8 @@ public MultipleValueOptionAttribute( bool ignoreOrderInChoices = false, string[]? aliases = null, ContextCaptureType contextCaptureType = ContextCaptureType.None, - int numberOfItemsToCapture = -1) + int numberOfItemsToCapture = -1, + bool addDefaultValueToDescription = false) : base( choices: null, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -71,7 +74,8 @@ public MultipleValueOptionAttribute( isFinal, ignoreCaseInChoices, aliases, - beforeParseChoices: null) + beforeParseChoices: null, + addDefaultValueToDescription: addDefaultValueToDescription) { IgnoreOrderInChoices = ignoreOrderInChoices; ContextCapture = CreateContextCapture(contextCaptureType, numberOfItemsToCapture); diff --git a/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs b/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs index 348a6af..9fe4b2c 100644 --- a/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs +++ b/Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs @@ -30,7 +30,8 @@ public ValueOptionAttribute( T[]? choices = null, string[]? beforeParseChoices = null, bool addChoicesToDescription = false, - bool addBeforeParseChoicesToDescription = false) + bool addBeforeParseChoicesToDescription = false, + bool addDefaultValueToDescription = false) : this( choices, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -44,7 +45,8 @@ public ValueOptionAttribute( aliases, beforeParseChoices, addChoicesToDescription, - addBeforeParseChoicesToDescription) + addBeforeParseChoicesToDescription, + addDefaultValueToDescription) { DefaultValue = new DefaultOptionValue(defaultValue); } @@ -62,7 +64,8 @@ public ValueOptionAttribute( string[]? aliases = null, string[]? beforeParseChoices = null, bool addChoicesToDescription = false, - bool addBeforeParseChoicesToDescription = false) + bool addBeforeParseChoicesToDescription = false, + bool addDefaultValueToDescription = false) : this( choices: null, longName ?? throw new ArgumentNullException(nameof(longName)), @@ -76,7 +79,8 @@ public ValueOptionAttribute( aliases, beforeParseChoices, addChoicesToDescription, - addBeforeParseChoicesToDescription) + addBeforeParseChoicesToDescription, + addDefaultValueToDescription) { } @@ -93,7 +97,8 @@ public ValueOptionAttribute( string[]? aliases = null, string[]? beforeParseChoices = null, bool addChoicesToDescription = false, - bool addBeforeParseChoicesToDescription = false) + bool addBeforeParseChoicesToDescription = false, + bool addDefaultValueToDescription = false) : base( longName ?? throw new ArgumentNullException(nameof(longName)), shortName ?? throw new ArgumentNullException(nameof(shortName)), @@ -109,6 +114,7 @@ public ValueOptionAttribute( IgnoreCaseInChoices = ignoreCaseInChoices; AddChoicesToDescription = addChoicesToDescription; AddBeforeParseChoicesToDescription = addBeforeParseChoicesToDescription; + AddDefaultValueToDescription = addDefaultValueToDescription; Choices = choices; BeforeParseChoices = beforeParseChoices; @@ -118,6 +124,7 @@ public ValueOptionAttribute( public bool IgnoreCaseInChoices { get; } public bool AddChoicesToDescription { get; } public bool AddBeforeParseChoicesToDescription { get; } + public bool AddDefaultValueToDescription { get; } public IEnumerable? Choices { get; } public IEnumerable? BeforeParseChoices { get; } @@ -149,6 +156,9 @@ public override ICommonOption CreateOption(object source, PropertyInfo propertyI null, t => propertyInfo.SetValue(source, t)); + if (AddDefaultValueToDescription) + valueOption.AddDefaultValueToDescription(); + if (AddBeforeParseChoicesToDescription && BeforeParseChoices is not null && BeforeParseChoices.Any()) valueOption.AddBeforeParseChoicesToDescription(); else if (AddChoicesToDescription && Choices is not null && Choices.Any()) diff --git a/Core/NetArgumentParser/Options/DefaultValueAlreadyAddedToDescriptionException.cs b/Core/NetArgumentParser/Options/DefaultValueAlreadyAddedToDescriptionException.cs new file mode 100644 index 0000000..8becdf0 --- /dev/null +++ b/Core/NetArgumentParser/Options/DefaultValueAlreadyAddedToDescriptionException.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace NetArgumentParser.Options; + +[Serializable] +public class DefaultValueAlreadyAddedToDescriptionException : Exception +{ + public DefaultValueAlreadyAddedToDescriptionException() { } + + public DefaultValueAlreadyAddedToDescriptionException(string? message) + : base(message) { } + + public DefaultValueAlreadyAddedToDescriptionException(string? message, Exception? innerException) + : base(message ?? GetDefaultMessage(), innerException) { } + +#if NET8_0_OR_GREATER + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.", DiagnosticId = "SYSLIB0051", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] +#endif + protected DefaultValueAlreadyAddedToDescriptionException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + private static string GetDefaultMessage() + { + return "Default value have already been added to description."; + } +} diff --git a/Core/NetArgumentParser/Options/ValueOption.cs b/Core/NetArgumentParser/Options/ValueOption.cs index 6d7b6fd..1b41223 100644 --- a/Core/NetArgumentParser/Options/ValueOption.cs +++ b/Core/NetArgumentParser/Options/ValueOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -15,6 +16,7 @@ public class ValueOption : CommonOption, IValueOption { private List _choices; private List _beforeParseChoices; + private bool _areDefaultValueAddedToDescription; private bool _areChoicesAddedToDescription; public ValueOption( @@ -152,6 +154,50 @@ public bool TrySetConverter(IValueConverter converter) return false; } + public void AddDefaultValueToDescription( + string separator = ", ", + string prefix = " [default=", + string postfix = "]", + string arraySeparator = "; ", + string arrayPrefix = "[", + string arrayPostfix = "]", + string nullPresenter = "null") + { + ExtendedArgumentNullException.ThrowIfNull(separator, nameof(separator)); + ExtendedArgumentNullException.ThrowIfNull(prefix, nameof(prefix)); + ExtendedArgumentNullException.ThrowIfNull(postfix, nameof(postfix)); + ExtendedArgumentNullException.ThrowIfNull(arraySeparator, nameof(arraySeparator)); + ExtendedArgumentNullException.ThrowIfNull(arrayPrefix, nameof(arrayPrefix)); + ExtendedArgumentNullException.ThrowIfNull(arrayPostfix, nameof(arrayPostfix)); + ExtendedArgumentNullException.ThrowIfNull(nullPresenter, nameof(nullPresenter)); + + if (_areDefaultValueAddedToDescription) + throw new DefaultValueAlreadyAddedToDescriptionException(); + + string defaultValueString; + + if (DefaultValue is null) + { + defaultValueString = nullPresenter; + } + else if (DefaultValue.Value is IEnumerable defaultValueEnumerable) + { + defaultValueString = ExtendedString.JoinWithExpand( + separator, + arraySeparator, + arrayPrefix, + arrayPostfix, + defaultValueEnumerable.Cast()); + } + else + { + defaultValueString = DefaultValue.Value?.ToString() ?? nullPresenter; + } + + Description += $"{prefix}{defaultValueString}{postfix}"; + _areDefaultValueAddedToDescription = true; + } + public void AddChoicesToDescription( string separator = ", ", string prefix = " (", From a8c8f4e53ce06de1043cc34da03f51f3ec135ab4 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Mon, 23 Feb 2026 21:43:31 +0300 Subject: [PATCH 2/3] Update examples --- .../Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/NetArgumentParser.Examples.ParserGenerationUsingAttributes/Program.cs b/Examples/NetArgumentParser.Examples.ParserGenerationUsingAttributes/Program.cs index 2e91850..b16dddd 100644 --- a/Examples/NetArgumentParser.Examples.ParserGenerationUsingAttributes/Program.cs +++ b/Examples/NetArgumentParser.Examples.ParserGenerationUsingAttributes/Program.cs @@ -105,7 +105,8 @@ public CustomParserConfig() choices: [FileMode.Create, FileMode.Open], beforeParseChoices: ["Create", "Open"], addChoicesToDescription: false, - addBeforeParseChoicesToDescription: true) + addBeforeParseChoicesToDescription: true, + addDefaultValueToDescription: false) ] [OptionGroup("complex-values", "", "")] public FileMode Mode { get; set; } @@ -178,7 +179,8 @@ public CustomParserConfig() choices: [0, 45, 90], beforeParseChoices: ["0", "45", "90"], addChoicesToDescription: true, - addBeforeParseChoicesToDescription: false) + addBeforeParseChoicesToDescription: false, + addDefaultValueToDescription: true) ] [OptionGroup("values", "", "")] public double? Angle { get; set; } From ac6816b8e43172797b13c421c3ca1c9d0f515398 Mon Sep 17 00:00:00 2001 From: yakovypg Date: Mon, 23 Feb 2026 21:45:03 +0300 Subject: [PATCH 3/3] Update documentation --- Documentation/AdditionalFeatures.md | 45 +++++++++++++++++++ .../ParserGenerationUsingAttributes.md | 8 ++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Documentation/AdditionalFeatures.md b/Documentation/AdditionalFeatures.md index 0c98d34..b2aa69a 100644 --- a/Documentation/AdditionalFeatures.md +++ b/Documentation/AdditionalFeatures.md @@ -7,6 +7,7 @@ * [Parse Known Arguments](#parse-known-arguments) * [Skip Arguments](#skip-arguments) * [Getting Info About Handled Options And Subcommands](#getting-info-about-handled-options-and-subcommands) +* [Add Default Value To Description](#add-default-value-to-description) * [Add Choices To Description](#add-choices-to-description) ## Negative Numbers & Scientific Notation @@ -116,6 +117,50 @@ ParseArgumentsResult result = parser.ParseKnownArguments(args, out _); // result.HandledSubcommands: compressSubcommand ``` +## Add Default Value To Description +You can add default value to the value option description automatically. To do this, you can use `AddDefaultValueToDescription()` method as follows: + +```cs +var angleOption = new ValueOption( + defaultValue: new DefaultOptionValue(45), + longName: "angle", + description: "Angle"); + +angleOption.AddDefaultValueToDescription(); + +var namesOption = new MultipleValueOption>( + defaultValue: new DefaultOptionValue>>([["Leo", "Max"], ["Eva", "Zoe"]]), + longName: "input", + description: "Names"); + +namesOption.AddDefaultValueToDescription( + separator: ", ", + prefix: " (default: ", + postfix: ")", + arraySeparator: "; ", + arrayPrefix: "[", + arrayPostfix: "]", + nullPresenter: "null"); + +var fileModeOption = new ValueOption( + defaultValue: new DefaultOptionValue(FileMode.Open), + longName: "mode", + description: "File mode"); + +fileModeOption.AddDefaultValueToDescription( + prefix: ". Default ", + postfix: string.Empty); + +// Angle [default=45] +Console.WriteLine(angleOption.Description); + +// Names (default: [John, Max]; [John1, Max1]) +Console.WriteLine(namesOption.Description); + +// File mode. Default Open +Console.WriteLine(fileModeOption.Description); +``` + ## Add Choices To Description You can add choices to the value option description automatically. To do this, you can use `AddChoicesToDescription()` method as follows: diff --git a/Documentation/ParserGenerationUsingAttributes.md b/Documentation/ParserGenerationUsingAttributes.md index bf80a89..34c31bb 100644 --- a/Documentation/ParserGenerationUsingAttributes.md +++ b/Documentation/ParserGenerationUsingAttributes.md @@ -74,7 +74,8 @@ internal class CustomParserConfig choices: [FileMode.Create, FileMode.Open], beforeParseChoices: ["Create", "Open"], addChoicesToDescription: false, - addBeforeParseChoicesToDescription: true) + addBeforeParseChoicesToDescription: true, + addDefaultValueToDescription: false) ] public FileMode Mode { get; set; } @@ -143,7 +144,8 @@ internal class CustomParserConfig choices: [0, 45, 90], beforeParseChoices: ["0", "45", "90"], addChoicesToDescription: true, - addBeforeParseChoicesToDescription: false) + addBeforeParseChoicesToDescription: false, + addDefaultValueToDescription: true) ] public double? Angle { get; set; } } @@ -151,7 +153,7 @@ internal class CustomParserConfig internal record Point(double X, double Y, double Z); ``` -Note that attributes allow you to add choices and before parse choices to the option description (if the corresponding option supports it) using `addChoicesToDescription` and `addBeforeParseChoicesToDescription` parameters, so you don't need to find the generated options to do this. +Note that attributes allow you to add default value, choices and before parse choices to the option description (if the corresponding option supports it) using `addDefaultValueToDescription`, `addChoicesToDescription`, and `addBeforeParseChoicesToDescription` parameters, so you don't need to find the generated options to do this. ### Group Attributes Goups can be configured using `OptionGroupAttribute` attribute. In addition to specifying the group header and description, you should specify the group ID. It is necessary for the correct placement of options, since groups can have the same header. Options that you want to put in the same group must be marked with an attribute with the same ID. You should't specify header and description for all group attributes with same id. It is enough to do this for only one attribute.