From f044b584502a5c01fd5509700cb71e36c06976ed Mon Sep 17 00:00:00 2001 From: Carl de Billy's personal bot - Autocarl Date: Thu, 25 Jun 2026 23:07:58 -0400 Subject: [PATCH 1/2] fix: keep default values out of global option help --- .../Help/HelpTextBuilder.Rendering.cs | 26 +++++++++++--- src/Repl.Core/ParsingOptions.cs | 34 ++----------------- src/Repl.Defaults/GlobalOptionsExtensions.cs | 2 +- .../Given_CustomGlobalOptions.cs | 14 ++++---- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/Repl.Core/Help/HelpTextBuilder.Rendering.cs b/src/Repl.Core/Help/HelpTextBuilder.Rendering.cs index 6f95fe6..20771b3 100644 --- a/src/Repl.Core/Help/HelpTextBuilder.Rendering.cs +++ b/src/Repl.Core/Help/HelpTextBuilder.Rendering.cs @@ -316,7 +316,27 @@ private static Type UnwrapAsyncReturnType(Type returnType) private static bool IsDefaultForType(object value, Type type) { - return ParsingOptions.IsDefaultForType(value, type); + if (type == typeof(bool)) + { + return value is false; + } + + if (type == typeof(int)) + { + return value is 0; + } + + if (type == typeof(long)) + { + return value is 0L; + } + + if (type == typeof(double)) + { + return value is 0.0d; + } + + return false; } private static string ResolveOptionPlaceholder(Type parameterType) @@ -605,10 +625,6 @@ private static string[][] BuildGlobalOptionRows(ParsingOptions parsingOptions) var description = string.IsNullOrWhiteSpace(option.Description) ? "Custom global option." : option.Description; - if (!string.IsNullOrWhiteSpace(option.DefaultValue)) - { - description = $"{description} [default: {option.DefaultValue}]"; - } return new[] { diff --git a/src/Repl.Core/ParsingOptions.cs b/src/Repl.Core/ParsingOptions.cs index a5af447..1737b6a 100644 --- a/src/Repl.Core/ParsingOptions.cs +++ b/src/Repl.Core/ParsingOptions.cs @@ -101,7 +101,7 @@ internal bool TryGetRouteConstraint(string name, out Func predicat /// Optional aliases. Values without prefix are normalized to --alias. /// Optional default value metadata. public void AddGlobalOption(string name, string[]? aliases = null, T? defaultValue = default) => - AddGlobalOptionCore(name, typeof(T), aliases, FormatDefaultValue(defaultValue, typeof(T))); + AddGlobalOptionCore(name, typeof(T), aliases, defaultValue?.ToString()); /// /// Registers a custom global option consumed before command routing. @@ -112,7 +112,7 @@ public void AddGlobalOption(string name, string[]? aliases = null, T? default /// Optional aliases. Values without prefix are normalized to --alias. /// Optional default value metadata. public void AddGlobalOption(string name, string? description, string[]? aliases = null, T? defaultValue = default) => - AddGlobalOptionCore(name, typeof(T), aliases, FormatDefaultValue(defaultValue, typeof(T)), description); + AddGlobalOptionCore(name, typeof(T), aliases, defaultValue?.ToString(), description); /// /// Registers a custom global option using a type or constraint name @@ -188,36 +188,6 @@ private static string BuildDuplicateGlobalOptionMessage(string name, Type? exist return $"A global option named '{name}' is already registered by {existingSource} and cannot also be registered by {newSource}."; } - internal static string? FormatDefaultValue(object? value, Type type) => - value is not null && !IsDefaultForType(value, type) - ? value.ToString() - : null; - - internal static bool IsDefaultForType(object value, Type type) - { - var effectiveType = Nullable.GetUnderlyingType(type) ?? type; - if (effectiveType == typeof(bool)) - { - return value is false; - } - - if (effectiveType == typeof(int)) - { - return value is 0; - } - - if (effectiveType == typeof(long)) - { - return value is 0L; - } - - if (effectiveType == typeof(double)) - { - return value is 0.0d; - } - - return false; - } private static Type ResolveConstraintOrTypeName( string constraintOrTypeName, diff --git a/src/Repl.Defaults/GlobalOptionsExtensions.cs b/src/Repl.Defaults/GlobalOptionsExtensions.cs index 125db8a..6a5f73f 100644 --- a/src/Repl.Defaults/GlobalOptionsExtensions.cs +++ b/src/Repl.Defaults/GlobalOptionsExtensions.cs @@ -34,7 +34,7 @@ public static class GlobalOptionsExtensions var optionAttr = property.GetCustomAttribute(); var name = optionAttr?.Name ?? ToKebabCase(property.Name); var aliases = optionAttr?.Aliases; - var defaultValue = ParsingOptions.FormatDefaultValue(property.GetValue(prototype), property.PropertyType); + var defaultValue = property.GetValue(prototype)?.ToString(); var description = property.GetCustomAttribute()?.Description; options.Parsing.AddGlobalOptionCore(name, property.PropertyType, aliases, defaultValue, description, typeof(T)); diff --git a/src/Repl.IntegrationTests/Given_CustomGlobalOptions.cs b/src/Repl.IntegrationTests/Given_CustomGlobalOptions.cs index 0a43035..aed9afc 100644 --- a/src/Repl.IntegrationTests/Given_CustomGlobalOptions.cs +++ b/src/Repl.IntegrationTests/Given_CustomGlobalOptions.cs @@ -51,8 +51,8 @@ public void When_RequestingRootHelp_Then_CustomGlobalOptionIsListedInGlobalOptio } [TestMethod] - [Description("Regression guard: verifies typed global option descriptions and defaults are rendered in root help.")] - public void When_RequestingRootHelpForTypedGlobalOptions_Then_DescriptionsAndDefaultsAreListed() + [Description("Regression guard: verifies typed global option descriptions are rendered in root help without adding default-value display.")] + public void When_RequestingRootHelpForTypedGlobalOptions_Then_DescriptionsAreListed() { var sut = ReplApp.Create() .UseGlobalOptions(); @@ -62,10 +62,10 @@ public void When_RequestingRootHelpForTypedGlobalOptions_Then_DescriptionsAndDef output.ExitCode.Should().Be(0); output.Text.Should().Contain("--tenant, -t"); - output.Text.Should().Contain("Tenant id used for all commands. [default: default]"); + output.Text.Should().Contain("Tenant id used for all commands."); output.Text.Should().Contain("--verbose, -v"); output.Text.Should().Contain("Enable verbose diagnostics for all commands."); - output.Text.Should().NotContain("Enable verbose diagnostics for all commands. [default: False]"); + output.Text.Should().NotContain("[default:"); } [TestMethod] @@ -76,15 +76,15 @@ public void When_RequestingRootHelpForExplicitGlobalOption_Then_DescriptionIsLis .Options(options => options.Parsing.AddGlobalOption( "tenant", description: "Tenant id used for all commands.", - aliases: ["-t"], - defaultValue: "default")); + aliases: ["-t"])); sut.Map("ping", () => "ok"); var output = ConsoleCaptureHelper.Capture(() => sut.Run(["--help", "--no-logo"])); output.ExitCode.Should().Be(0); output.Text.Should().Contain("--tenant, -t"); - output.Text.Should().Contain("Tenant id used for all commands. [default: default]"); + output.Text.Should().Contain("Tenant id used for all commands."); + output.Text.Should().NotContain("[default:"); } private sealed class DemoGlobals From 5bbfac5cb1b11ef6557f369a4cce05db4bf7e9a7 Mon Sep 17 00:00:00 2001 From: Carl de Billy's personal bot - Autocarl Date: Thu, 25 Jun 2026 23:20:24 -0400 Subject: [PATCH 2/2] fix: preserve caller defaults for value-typed globals --- src/Repl.Core/ParsingOptions.cs | 34 +++++++++++++++++-- src/Repl.Defaults/GlobalOptionsExtensions.cs | 2 +- src/Repl.Tests/Given_GlobalOptionsAccessor.cs | 24 +++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/Repl.Core/ParsingOptions.cs b/src/Repl.Core/ParsingOptions.cs index 1737b6a..a5af447 100644 --- a/src/Repl.Core/ParsingOptions.cs +++ b/src/Repl.Core/ParsingOptions.cs @@ -101,7 +101,7 @@ internal bool TryGetRouteConstraint(string name, out Func predicat /// Optional aliases. Values without prefix are normalized to --alias. /// Optional default value metadata. public void AddGlobalOption(string name, string[]? aliases = null, T? defaultValue = default) => - AddGlobalOptionCore(name, typeof(T), aliases, defaultValue?.ToString()); + AddGlobalOptionCore(name, typeof(T), aliases, FormatDefaultValue(defaultValue, typeof(T))); /// /// Registers a custom global option consumed before command routing. @@ -112,7 +112,7 @@ public void AddGlobalOption(string name, string[]? aliases = null, T? default /// Optional aliases. Values without prefix are normalized to --alias. /// Optional default value metadata. public void AddGlobalOption(string name, string? description, string[]? aliases = null, T? defaultValue = default) => - AddGlobalOptionCore(name, typeof(T), aliases, defaultValue?.ToString(), description); + AddGlobalOptionCore(name, typeof(T), aliases, FormatDefaultValue(defaultValue, typeof(T)), description); /// /// Registers a custom global option using a type or constraint name @@ -188,6 +188,36 @@ private static string BuildDuplicateGlobalOptionMessage(string name, Type? exist return $"A global option named '{name}' is already registered by {existingSource} and cannot also be registered by {newSource}."; } + internal static string? FormatDefaultValue(object? value, Type type) => + value is not null && !IsDefaultForType(value, type) + ? value.ToString() + : null; + + internal static bool IsDefaultForType(object value, Type type) + { + var effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType == typeof(bool)) + { + return value is false; + } + + if (effectiveType == typeof(int)) + { + return value is 0; + } + + if (effectiveType == typeof(long)) + { + return value is 0L; + } + + if (effectiveType == typeof(double)) + { + return value is 0.0d; + } + + return false; + } private static Type ResolveConstraintOrTypeName( string constraintOrTypeName, diff --git a/src/Repl.Defaults/GlobalOptionsExtensions.cs b/src/Repl.Defaults/GlobalOptionsExtensions.cs index 6a5f73f..125db8a 100644 --- a/src/Repl.Defaults/GlobalOptionsExtensions.cs +++ b/src/Repl.Defaults/GlobalOptionsExtensions.cs @@ -34,7 +34,7 @@ public static class GlobalOptionsExtensions var optionAttr = property.GetCustomAttribute(); var name = optionAttr?.Name ?? ToKebabCase(property.Name); var aliases = optionAttr?.Aliases; - var defaultValue = property.GetValue(prototype)?.ToString(); + var defaultValue = ParsingOptions.FormatDefaultValue(property.GetValue(prototype), property.PropertyType); var description = property.GetCustomAttribute()?.Description; options.Parsing.AddGlobalOptionCore(name, property.PropertyType, aliases, defaultValue, description, typeof(T)); diff --git a/src/Repl.Tests/Given_GlobalOptionsAccessor.cs b/src/Repl.Tests/Given_GlobalOptionsAccessor.cs index 2129a3f..59b9866 100644 --- a/src/Repl.Tests/Given_GlobalOptionsAccessor.cs +++ b/src/Repl.Tests/Given_GlobalOptionsAccessor.cs @@ -67,6 +67,30 @@ public void When_OptionNotProvidedButRegisteredDefault_Then_GetValueReturnsRegis sut.GetValue("port").Should().Be(3000); } + [TestMethod] + [Description("GetValue returns caller default when a value-typed option has no explicit registration default.")] + public void When_ValueTypeOptionNotProvidedAndNoRegisteredDefault_Then_GetValueReturnsCallerDefault() + { + var parsing = new ParsingOptions(); + parsing.AddGlobalOption("port"); + var sut = new GlobalOptionsSnapshot(parsing); + sut.Update(new Dictionary>(StringComparer.OrdinalIgnoreCase)); + + sut.GetValue("port", 8080).Should().Be(8080); + } + + [TestMethod] + [Description("GetValue returns caller default when a described value-typed option has no explicit registration default.")] + public void When_DescribedValueTypeOptionNotProvidedAndNoRegisteredDefault_Then_GetValueReturnsCallerDefault() + { + var parsing = new ParsingOptions(); + parsing.AddGlobalOption("port", "Port used by the server."); + var sut = new GlobalOptionsSnapshot(parsing); + sut.Update(new Dictionary>(StringComparer.OrdinalIgnoreCase)); + + sut.GetValue("port", 8080).Should().Be(8080); + } + [TestMethod] [Description("HasValue returns false before parsing.")] public void When_NeverUpdated_Then_HasValueReturnsFalse()