From 54f1f043b90646aa9b9c09b0656f69de172e7584 Mon Sep 17 00:00:00 2001 From: Dark Daskin Date: Wed, 31 Dec 2025 06:49:05 +0300 Subject: [PATCH 1/2] allow to use arguments Renamed corresponding types to use option/argument/symbol where appropriate. --- .../CommandBuilderTests.cs | 31 +++++++++++------ .../ExpressionExtensionTests.cs | 10 +++--- .../SystemCommandLine.Extensions.Tests.csproj | 1 + .../Builders/CommandArgumentBuilder.cs | 13 ++++---- .../CommandArgumentBuilderWithMapping.cs | 33 ++++++------------- .../Builders/CommandBuilder.cs | 9 +++-- .../Builders/CommandBuilderWithMapping.cs | 11 +++++-- .../Builders/CommandOptionBuilder.cs | 21 ++++++++++++ .../CommandOptionBuilderWithMapping.cs | 28 ++++++++++++++++ .../CommandSymbolBuilderWithMapping.cs | 19 +++++++++++ .../CommandArgumentMapper.cs | 8 ----- .../CommandSymbolMapper.cs | 8 +++++ .../ExpressionExtensions.cs | 11 +++---- .../ICommandArgumentBuilder.cs | 2 +- .../ICommandArgumentBuilderWithMapping.cs | 2 +- .../ICommandBuilder.cs | 5 +-- .../ICommandBuilderWithMapping.cs | 3 +- .../ICommandOptionBuilder.cs | 9 +++++ .../ICommandOptionBuilderWithMapping.cs | 11 +++++++ .../IUseCommandBuilder.cs | 2 +- .../NameFormatExtensions.cs | 8 ++--- .../ServiceCollectionExtensions.cs | 10 +++--- .../Properties/launchSettings.json | 2 +- .../RootCommand/GreetCommand/Greet.cs | 9 +++-- src/TestConsoleApp/RootCommand/Root.cs | 4 +-- 25 files changed, 184 insertions(+), 86 deletions(-) create mode 100644 src/SystemCommandLine.Extensions/Builders/CommandOptionBuilder.cs create mode 100644 src/SystemCommandLine.Extensions/Builders/CommandOptionBuilderWithMapping.cs create mode 100644 src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs delete mode 100644 src/SystemCommandLine.Extensions/CommandArgumentMapper.cs create mode 100644 src/SystemCommandLine.Extensions/CommandSymbolMapper.cs create mode 100644 src/SystemCommandLine.Extensions/ICommandOptionBuilder.cs create mode 100644 src/SystemCommandLine.Extensions/ICommandOptionBuilderWithMapping.cs diff --git a/src/SystemCommandLine.Extensions.Tests/CommandBuilderTests.cs b/src/SystemCommandLine.Extensions.Tests/CommandBuilderTests.cs index fa3f3c7..6965acc 100644 --- a/src/SystemCommandLine.Extensions.Tests/CommandBuilderTests.cs +++ b/src/SystemCommandLine.Extensions.Tests/CommandBuilderTests.cs @@ -10,7 +10,7 @@ public class CommandBuilderTests { private class TestRootCommand : RootCommand, IUseCommandBuilder { - public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, ArgumentMapperRegistration mapperRegistration) + public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, SymbolMapperRegistration mapperRegistration) { return new TestRootCommand() { serviceProvider.GetRequiredService() @@ -20,7 +20,7 @@ public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, A private class TestCommand : Command, IUseCommandBuilder { - public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test", "A test command") + public TestCommand(SymbolMapperRegistration mapperRegistration) : base("test", "A test command") { this.UseCommandBuilder().WithMapping(mapperRegistration) .NewOption(x => x.Name).Configure(o => @@ -32,6 +32,10 @@ public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test", { o.Description = "Name with property default"; }).AddToCommand() + .NewArgument(x => x.NameArgument).Configure(o => + { + o.Description = "Name argument"; + }).AddToCommand() .NewOption(x => x.Count).Configure(o => { o.Description = "Count for the test"; @@ -41,14 +45,20 @@ public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test", { o.Description = "An option that is not mapped to the options class"; o.DefaultValueFactory = _ => "!"; + }).AddToCommand() + .NewArgument("NotMappedArgument").Configure(o => + { + o.Description = "An argument that is not mapped to the options class"; + o.DefaultValueFactory = _ => "!"; }).AddToCommand(); } - public static TestCommand CommandFactory(IServiceProvider serviceProvider, ArgumentMapperRegistration mapperRegistration) => new(mapperRegistration); + public static TestCommand CommandFactory(IServiceProvider serviceProvider, SymbolMapperRegistration mapperRegistration) => new(mapperRegistration); public class TestCommandOptions { public required string Name { get; set; } public string NameWithDefault { get; set; } = "PropertyDefault"; + public required string NameArgument { get; set; } public int Count { get; set; } } public class TestHandler(IOptions options) : SynchronousCommandLineAction @@ -70,9 +80,10 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio private static int Handler(string handlerType, ParseResult parseResult, IOptions options) { string notMappedOption = parseResult.GetRequiredValue(NameFormatExtensions.ToKebabCase("--", nameof(notMappedOption))); + string notMappedArgument = parseResult.GetRequiredValue(NameFormatExtensions.ToKebabCase(nameof(notMappedArgument))); parseResult.InvocationConfiguration - .Output.WriteLine($"Running {handlerType} test '{options.Value.Name}' '{options.Value.NameWithDefault}' {options.Value.Count} times {notMappedOption}"); + .Output.WriteLine($"Running {handlerType} test '{options.Value.Name}' '{options.Value.NameWithDefault}' '{options.Value.NameArgument}' {options.Value.Count} times {notMappedOption} {notMappedArgument}"); parseResult.InvocationConfiguration .Error.WriteLine($"Error message"); @@ -102,7 +113,7 @@ private static ServiceProvider GetServiceProviderWithHandler(string args) [Fact] public void Should_run_synchronous_handler() { - using var serviceProvider = GetServiceProviderWithHandler("test --name MyTest --count 5 --not-mapped-option !!!"); + using var serviceProvider = GetServiceProviderWithHandler("test --name MyTest --count 5 --not-mapped-option !!! foo bar"); ParseResult parserResult = serviceProvider.GetRequiredService(); InvocationConfiguration configuration = new() @@ -113,13 +124,13 @@ public void Should_run_synchronous_handler() int result = parserResult.Invoke(configuration); Assert.Matches(@"Error message", configuration.Error.ToString()); Assert.Equal(42, result); - Assert.Matches(@"Running sync test 'MyTest' 'PropertyDefault' 5 times !!!", configuration.Output.ToString()); + Assert.Matches(@"Running sync test 'MyTest' 'PropertyDefault' 'foo' 5 times !!! bar", configuration.Output.ToString()); } [Fact] public async Task Should_run_asynchronous_handler() { - using ServiceProvider serviceProvider = GetServiceProviderWithAsyncHandler("test --name MyTest --count 5 --not-mapped-option !!!"); + using ServiceProvider serviceProvider = GetServiceProviderWithAsyncHandler("test --name MyTest --count 5 --not-mapped-option !!! foo bar"); ParseResult parserResult = serviceProvider.GetRequiredService(); @@ -132,13 +143,13 @@ public async Task Should_run_asynchronous_handler() Assert.Matches(@"Error message", configuration.Error.ToString()); Assert.Equal(42, result); - Assert.Matches(@"Running async test 'MyTest' 'PropertyDefault' 5 times !!!", configuration.Output.ToString()); + Assert.Matches(@"Running async test 'MyTest' 'PropertyDefault' 'foo' 5 times !!! bar", configuration.Output.ToString()); } [Fact] public void Should_allow_option_and_property_default_values() { - using ServiceProvider serviceProvider = GetServiceProviderWithHandler("test"); + using ServiceProvider serviceProvider = GetServiceProviderWithHandler("test foo"); ParseResult parserResult = serviceProvider.GetRequiredService(); @@ -150,6 +161,6 @@ public void Should_allow_option_and_property_default_values() int result = parserResult.Invoke(configuration); Assert.Equal(42, result); - Assert.Matches(@"Running sync test 'OptionDefault' 'PropertyDefault' 1 times !", configuration.Output.ToString()); + Assert.Matches(@"Running sync test 'OptionDefault' 'PropertyDefault' 'foo' 1 times ! !", configuration.Output.ToString()); } } diff --git a/src/SystemCommandLine.Extensions.Tests/ExpressionExtensionTests.cs b/src/SystemCommandLine.Extensions.Tests/ExpressionExtensionTests.cs index a87dc02..af86e56 100644 --- a/src/SystemCommandLine.Extensions.Tests/ExpressionExtensionTests.cs +++ b/src/SystemCommandLine.Extensions.Tests/ExpressionExtensionTests.cs @@ -25,7 +25,7 @@ public void GetPropertyName_returns_property_name() public void CreateArgumentMapper_sets_reference_type_property_value() { // Arrange - Action mapper = ExpressionExtensions.CreateArgumentValueMapper(h => h.Name); + Action mapper = ExpressionExtensions.CreateSymbolValueMapper(h => h.Name); Holder holder = new(); // Act @@ -39,7 +39,7 @@ public void CreateArgumentMapper_sets_reference_type_property_value() public void CreateArgumentMapper_does_not_set_null_value() { // Arrange - Action mapper = ExpressionExtensions.CreateArgumentValueMapper(h => h.Name); + Action mapper = ExpressionExtensions.CreateSymbolValueMapper(h => h.Name); Holder holder = new(); // Act @@ -53,7 +53,7 @@ public void CreateArgumentMapper_does_not_set_null_value() public void CreateArgumentMapper_sets_value_type_property_value() { // Arrange - Action mapper = ExpressionExtensions.CreateArgumentValueMapper(h => h.Number); + Action mapper = ExpressionExtensions.CreateSymbolValueMapper(h => h.Number); Holder holder = new(); // Act @@ -68,7 +68,7 @@ public void CreateArgumentMapper_throws_when_setter_is_not_public() { // Act ArgumentException ex = Assert.Throws(() => - ExpressionExtensions.CreateArgumentValueMapper(h => h.WithPrivateSetter)); + ExpressionExtensions.CreateSymbolValueMapper(h => h.WithPrivateSetter)); // Assert Assert.Equal("Property Holder.WithPrivateSetter has no accessible setter.", ex.Message); @@ -79,7 +79,7 @@ public void CreateArgumentMapper_throws_when_property_has_no_setter() { // Act ArgumentException ex = Assert.Throws(() => - ExpressionExtensions.CreateArgumentValueMapper(h => h.ReadOnly)); + ExpressionExtensions.CreateSymbolValueMapper(h => h.ReadOnly)); // Assert Assert.Equal("Property Holder.ReadOnly has no accessible setter.", ex.Message); diff --git a/src/SystemCommandLine.Extensions.Tests/SystemCommandLine.Extensions.Tests.csproj b/src/SystemCommandLine.Extensions.Tests/SystemCommandLine.Extensions.Tests.csproj index 7f99cee..de07fd9 100644 --- a/src/SystemCommandLine.Extensions.Tests/SystemCommandLine.Extensions.Tests.csproj +++ b/src/SystemCommandLine.Extensions.Tests/SystemCommandLine.Extensions.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilder.cs b/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilder.cs index 1925e24..2b41e49 100644 --- a/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilder.cs +++ b/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilder.cs @@ -2,19 +2,20 @@ namespace SystemCommandLine.Extensions.Builders; -internal class CommandArgumentBuilder(ICommandBuilder commandBuilder, TCommand command, string name) : ICommandArgumentBuilder where TCommand : Command, IUseCommandBuilder +internal class CommandArgumentBuilder(ICommandBuilder commandBuilder, TCommand command, string name) : + ICommandArgumentBuilder where TCommand : Command, IUseCommandBuilder { - protected readonly Option option = new(NameFormatExtensions.ToKebabCase("--", name)); + protected readonly Argument argument = new(NameFormatExtensions.ToKebabCase(name)); - public virtual ICommandArgumentBuilder Configure(Action> value) + public virtual ICommandArgumentBuilder Configure(Action> value) { - value.Invoke(option); + value.Invoke(argument); return this; } public virtual ICommandBuilder AddToCommand() { - command.Add(option); + command.Add(argument); return commandBuilder; } -} +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilderWithMapping.cs index ecbe5eb..54ef43b 100644 --- a/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilderWithMapping.cs +++ b/src/SystemCommandLine.Extensions/Builders/CommandArgumentBuilderWithMapping.cs @@ -3,39 +3,26 @@ namespace SystemCommandLine.Extensions.Builders; -internal class CommandArgumentBuilderWithMapping(TCommand command, ICommandBuilderWithMapping commandHandlerBuilder, Expression> propertyExpression, ArgumentMapperRegistration mapperRegistration) : ICommandArgumentBuilderWithMapping where TCommand : Command, IUseCommandBuilder +internal class CommandArgumentBuilderWithMapping( + TCommand command, ICommandBuilderWithMapping commandHandlerBuilder, Expression> propertyExpression, SymbolMapperRegistration mapperRegistration) : + CommandSymbolBuilderWithMapping(propertyExpression, mapperRegistration), + ICommandArgumentBuilderWithMapping where TCommand : Command, IUseCommandBuilder where TOptionHolder : class { - private readonly Option option = new(ToKebabCase(propertyExpression.GetPropertyName())); + private readonly Argument argument = new(NameFormatExtensions.ToKebabCase(propertyExpression.GetPropertyName())); - private static string ToKebabCase(string optionName) + public ICommandArgumentBuilderWithMapping Configure(Action> value) { - return NameFormatExtensions.ToKebabCase("--", optionName); - } - public ICommandArgumentBuilderWithMapping Configure(Action> value) - { - value.Invoke(option); + value.Invoke(argument); return this; } public ICommandBuilderWithMapping AddToCommand() { - command.Add(option); + command.Add(argument); - RegisterArgumentMapper(); + RegisterSymbolMapper(argument); return commandHandlerBuilder; } - - private void RegisterArgumentMapper() - { - Action argumentValueMapper = propertyExpression.CreateArgumentValueMapper(); - - void argumentMapper(ParseResult parsedResult, object options) - { - argumentValueMapper((TOptionHolder)options, parsedResult.GetValue(option.Name)); - } - - mapperRegistration(argumentMapper); - } -} +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/Builders/CommandBuilder.cs b/src/SystemCommandLine.Extensions/Builders/CommandBuilder.cs index 82e1f72..3dfb820 100644 --- a/src/SystemCommandLine.Extensions/Builders/CommandBuilder.cs +++ b/src/SystemCommandLine.Extensions/Builders/CommandBuilder.cs @@ -4,12 +4,17 @@ namespace SystemCommandLine.Extensions.Builders; internal class CommandBuilder(TCommand command) : ICommandBuilder where TCommand : Command, IUseCommandBuilder { - public ICommandArgumentBuilder NewOption(string name) + public ICommandArgumentBuilder NewArgument(string name) { return new CommandArgumentBuilder(this, command, name); } - public ICommandBuilderWithMapping WithMapping(ArgumentMapperRegistration mapperRegistration) + public ICommandOptionBuilder NewOption(string name) + { + return new CommandOptionBuilder(this, command, name); + } + + public ICommandBuilderWithMapping WithMapping(SymbolMapperRegistration mapperRegistration) where TOptionHolder : class { return new CommandBuilderWithMapping(this, command, mapperRegistration); diff --git a/src/SystemCommandLine.Extensions/Builders/CommandBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/Builders/CommandBuilderWithMapping.cs index f5109ba..309706c 100644 --- a/src/SystemCommandLine.Extensions/Builders/CommandBuilderWithMapping.cs +++ b/src/SystemCommandLine.Extensions/Builders/CommandBuilderWithMapping.cs @@ -3,12 +3,19 @@ namespace SystemCommandLine.Extensions.Builders; -internal class CommandBuilderWithMapping(ICommandBuilder commandBuilder, TCommand command, ArgumentMapperRegistration mapperRegistration) : ICommandBuilderWithMapping where TCommand : Command, IUseCommandBuilder +internal class CommandBuilderWithMapping(ICommandBuilder commandBuilder, TCommand command, SymbolMapperRegistration mapperRegistration) : + ICommandBuilderWithMapping where TCommand : Command, IUseCommandBuilder where TOptionHolder : class { - public ICommandArgumentBuilderWithMapping NewOption(Expression> propertyExpression) + public ICommandArgumentBuilderWithMapping NewArgument(Expression> propertyExpression) { return new CommandArgumentBuilderWithMapping(command, this, propertyExpression, mapperRegistration); } + + public ICommandOptionBuilderWithMapping NewOption(Expression> propertyExpression) + { + return new CommandOptionBuilderWithMapping(command, this, propertyExpression, mapperRegistration); + } + public ICommandBuilder CommandBuilder() => commandBuilder; } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilder.cs b/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilder.cs new file mode 100644 index 0000000..3369d16 --- /dev/null +++ b/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilder.cs @@ -0,0 +1,21 @@ +using System.CommandLine; + +namespace SystemCommandLine.Extensions.Builders; + +internal class CommandOptionBuilder(ICommandBuilder commandBuilder, TCommand command, string name) : + ICommandOptionBuilder where TCommand : Command, IUseCommandBuilder +{ + protected readonly Option option = new(NameFormatExtensions.ToKebabCase("--", name)); + + public virtual ICommandOptionBuilder Configure(Action> value) + { + value.Invoke(option); + return this; + } + + public virtual ICommandBuilder AddToCommand() + { + command.Add(option); + return commandBuilder; + } +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilderWithMapping.cs new file mode 100644 index 0000000..4ed69db --- /dev/null +++ b/src/SystemCommandLine.Extensions/Builders/CommandOptionBuilderWithMapping.cs @@ -0,0 +1,28 @@ +using System.CommandLine; +using System.Linq.Expressions; + +namespace SystemCommandLine.Extensions.Builders; + +internal class CommandOptionBuilderWithMapping( + TCommand command, ICommandBuilderWithMapping commandHandlerBuilder, Expression> propertyExpression, SymbolMapperRegistration mapperRegistration) : + CommandSymbolBuilderWithMapping(propertyExpression, mapperRegistration), + ICommandOptionBuilderWithMapping where TCommand : Command, IUseCommandBuilder + where TOptionHolder : class +{ + private readonly Option option = new(NameFormatExtensions.ToKebabCase("--", propertyExpression.GetPropertyName())); + + public ICommandOptionBuilderWithMapping Configure(Action> value) + { + value.Invoke(option); + return this; + } + + public ICommandBuilderWithMapping AddToCommand() + { + command.Add(option); + + RegisterSymbolMapper(option); + + return commandHandlerBuilder; + } +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs new file mode 100644 index 0000000..2c534ad --- /dev/null +++ b/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs @@ -0,0 +1,19 @@ +using System.CommandLine; +using System.Linq.Expressions; + +namespace SystemCommandLine.Extensions.Builders; + +internal abstract class CommandSymbolBuilderWithMapping(Expression> propertyExpression, SymbolMapperRegistration mapperRegistration) where TOptionHolder : class +{ + protected void RegisterSymbolMapper(Symbol symbol) + { + Action symbolValueMapper = propertyExpression.CreateSymbolValueMapper(); + + void symbolMapper(ParseResult parsedResult, object options) + { + symbolValueMapper((TOptionHolder)options, parsedResult.GetValue(symbol.Name)); + } + + mapperRegistration(symbolMapper); + } +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/CommandArgumentMapper.cs b/src/SystemCommandLine.Extensions/CommandArgumentMapper.cs deleted file mode 100644 index bdf96d1..0000000 --- a/src/SystemCommandLine.Extensions/CommandArgumentMapper.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.CommandLine; - -namespace SystemCommandLine.Extensions; - -public delegate void ArgumentMapper(ParseResult parseResult, object options); -public delegate void ArgumentMapperRegistration(ArgumentMapper mapper); - -internal class CommandArgumentMapper : List { } diff --git a/src/SystemCommandLine.Extensions/CommandSymbolMapper.cs b/src/SystemCommandLine.Extensions/CommandSymbolMapper.cs new file mode 100644 index 0000000..76c1f8e --- /dev/null +++ b/src/SystemCommandLine.Extensions/CommandSymbolMapper.cs @@ -0,0 +1,8 @@ +using System.CommandLine; + +namespace SystemCommandLine.Extensions; + +public delegate void SymbolMapper(ParseResult parseResult, object options); +public delegate void SymbolMapperRegistration(SymbolMapper mapper); + +internal class CommandSymbolMapper : List { } diff --git a/src/SystemCommandLine.Extensions/ExpressionExtensions.cs b/src/SystemCommandLine.Extensions/ExpressionExtensions.cs index 4a67f36..fcc4971 100644 --- a/src/SystemCommandLine.Extensions/ExpressionExtensions.cs +++ b/src/SystemCommandLine.Extensions/ExpressionExtensions.cs @@ -6,12 +6,14 @@ namespace SystemCommandLine.Extensions; public static class ExpressionExtensions { - public static string GetPropertyName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptionHolder, TOption>(this Expression> propertyExpression) + public static string GetPropertyName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptionHolder, TOption>( + this Expression> propertyExpression) { return propertyExpression.ExtractProperty().Name; } - public static Action CreateArgumentValueMapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptionHolder, TOption>(this Expression> propertyExpression) where TOptionHolder : class + public static Action CreateSymbolValueMapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptionHolder, TOption>( + this Expression> propertyExpression) where TOptionHolder : class { PropertyInfo propertyInfo = propertyExpression.ExtractProperty(); @@ -44,8 +46,5 @@ private static PropertyInfo ExtractProperty( throw new ArgumentException("Expression must be a property access like t => t.Property."); } - private static bool IsWritable([NotNullWhen(true)] this MethodInfo? setMethod) - { - return setMethod is { } && setMethod.IsPublic; - } + private static bool IsWritable([NotNullWhen(true)] this MethodInfo? setMethod) => setMethod is { IsPublic: true }; } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandArgumentBuilder.cs b/src/SystemCommandLine.Extensions/ICommandArgumentBuilder.cs index 257e6b1..9b925d7 100644 --- a/src/SystemCommandLine.Extensions/ICommandArgumentBuilder.cs +++ b/src/SystemCommandLine.Extensions/ICommandArgumentBuilder.cs @@ -5,5 +5,5 @@ namespace SystemCommandLine.Extensions; public interface ICommandArgumentBuilder where TCommand : Command, IUseCommandBuilder { ICommandBuilder AddToCommand(); - ICommandArgumentBuilder Configure(Action> value); + ICommandArgumentBuilder Configure(Action> value); } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandArgumentBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/ICommandArgumentBuilderWithMapping.cs index 237aa3a..45a8e4d 100644 --- a/src/SystemCommandLine.Extensions/ICommandArgumentBuilderWithMapping.cs +++ b/src/SystemCommandLine.Extensions/ICommandArgumentBuilderWithMapping.cs @@ -7,5 +7,5 @@ public interface ICommandArgumentBuilderWithMapping AddToCommand(); - ICommandArgumentBuilderWithMapping Configure(Action> value); + ICommandArgumentBuilderWithMapping Configure(Action> value); } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandBuilder.cs b/src/SystemCommandLine.Extensions/ICommandBuilder.cs index 230a866..01c2ce4 100644 --- a/src/SystemCommandLine.Extensions/ICommandBuilder.cs +++ b/src/SystemCommandLine.Extensions/ICommandBuilder.cs @@ -4,6 +4,7 @@ namespace SystemCommandLine.Extensions; public interface ICommandBuilder where TCommand : Command, IUseCommandBuilder { - ICommandArgumentBuilder NewOption(string name); - ICommandBuilderWithMapping WithMapping(ArgumentMapperRegistration mapperRegistration) where TOptionHolder : class; + ICommandArgumentBuilder NewArgument(string name); + ICommandOptionBuilder NewOption(string name); + ICommandBuilderWithMapping WithMapping(SymbolMapperRegistration mapperRegistration) where TOptionHolder : class; } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/ICommandBuilderWithMapping.cs index 357894a..a70d549 100644 --- a/src/SystemCommandLine.Extensions/ICommandBuilderWithMapping.cs +++ b/src/SystemCommandLine.Extensions/ICommandBuilderWithMapping.cs @@ -8,5 +8,6 @@ public interface ICommandBuilderWithMapping where TOptionHolder : class { ICommandBuilder CommandBuilder(); - ICommandArgumentBuilderWithMapping NewOption(Expression> propertyExpression); + ICommandArgumentBuilderWithMapping NewArgument(Expression> propertyExpression); + ICommandOptionBuilderWithMapping NewOption(Expression> propertyExpression); } \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandOptionBuilder.cs b/src/SystemCommandLine.Extensions/ICommandOptionBuilder.cs new file mode 100644 index 0000000..f12f1f8 --- /dev/null +++ b/src/SystemCommandLine.Extensions/ICommandOptionBuilder.cs @@ -0,0 +1,9 @@ +using System.CommandLine; + +namespace SystemCommandLine.Extensions; + +public interface ICommandOptionBuilder where TCommand : Command, IUseCommandBuilder +{ + ICommandBuilder AddToCommand(); + ICommandOptionBuilder Configure(Action> value); +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/ICommandOptionBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/ICommandOptionBuilderWithMapping.cs new file mode 100644 index 0000000..1b01255 --- /dev/null +++ b/src/SystemCommandLine.Extensions/ICommandOptionBuilderWithMapping.cs @@ -0,0 +1,11 @@ +using System.CommandLine; + +namespace SystemCommandLine.Extensions; + +public interface ICommandOptionBuilderWithMapping + where TCommand : Command, IUseCommandBuilder + where TOptionHolder : class +{ + ICommandBuilderWithMapping AddToCommand(); + ICommandOptionBuilderWithMapping Configure(Action> value); +} \ No newline at end of file diff --git a/src/SystemCommandLine.Extensions/IUseCommandBuilder.cs b/src/SystemCommandLine.Extensions/IUseCommandBuilder.cs index bdeff15..763ec5b 100644 --- a/src/SystemCommandLine.Extensions/IUseCommandBuilder.cs +++ b/src/SystemCommandLine.Extensions/IUseCommandBuilder.cs @@ -6,5 +6,5 @@ namespace System.CommandLine; public interface IUseCommandBuilder where TCommand : Command { - static abstract TCommand CommandFactory(IServiceProvider serviceProvider, ArgumentMapperRegistration mapperRegistration); + static abstract TCommand CommandFactory(IServiceProvider serviceProvider, SymbolMapperRegistration mapperRegistration); } diff --git a/src/SystemCommandLine.Extensions/NameFormatExtensions.cs b/src/SystemCommandLine.Extensions/NameFormatExtensions.cs index fe26430..6e46290 100644 --- a/src/SystemCommandLine.Extensions/NameFormatExtensions.cs +++ b/src/SystemCommandLine.Extensions/NameFormatExtensions.cs @@ -4,11 +4,9 @@ namespace SystemCommandLine.Extensions; public static partial class NameFormatExtensions { - public static string ToKebabCase(string prefix, string name) - { - name = MyRegex().Replace(name, "-$1").Trim('-').ToLower(); - return $"{prefix}{name}"; - } + public static string ToKebabCase(string prefix, string name) => $"{prefix}{ToKebabCase(name)}"; + + public static string ToKebabCase(string name) => MyRegex().Replace(name, "-$1").Trim('-').ToLower(); [GeneratedRegex("(? BindCommand(this Opt where TCommand : Command, IUseCommandBuilder where TOptions : class { - return optionsBuilder.Configure>, ParseResult>((options, command, commandArgumentMappers, parseResult) => + return optionsBuilder.Configure>, ParseResult>((options, command, commandArgumentMappers, parseResult) => { - foreach (ArgumentMapper argumentMapper in commandArgumentMappers.Value) + foreach (SymbolMapper argumentMapper in commandArgumentMappers.Value) { argumentMapper(parseResult, options); } @@ -29,7 +29,7 @@ public static IServiceCollection AddBoundToCommandOptions(th { return services .AddOptions().BindCommand().Services - .AddOptions>().Services; + .AddOptions>().Services; } public static IServiceCollection AddRootCommand(this IServiceCollection services, string[] args) @@ -78,8 +78,8 @@ public static TCommand SetHandler(this TCommand command, ISe return command; } - private static ArgumentMapperRegistration GetArgumentMapperRegistration(IServiceProvider sp) where TCommand : Command, IUseCommandBuilder + private static SymbolMapperRegistration GetArgumentMapperRegistration(IServiceProvider sp) where TCommand : Command, IUseCommandBuilder { - return sp.GetRequiredService>>().Value.Add; + return sp.GetRequiredService>>().Value.Add; } } diff --git a/src/TestConsoleApp/Properties/launchSettings.json b/src/TestConsoleApp/Properties/launchSettings.json index e34e6b9..f72127c 100644 --- a/src/TestConsoleApp/Properties/launchSettings.json +++ b/src/TestConsoleApp/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "TestConsoleApp": { "commandName": "Project", - "commandLineArgs": "greet --name John --shout --times 5 --log-event-level Debug" + "commandLineArgs": "greet John --shout --times 5 --log-event-level Debug" } } } \ No newline at end of file diff --git a/src/TestConsoleApp/RootCommand/GreetCommand/Greet.cs b/src/TestConsoleApp/RootCommand/GreetCommand/Greet.cs index 1afb4f3..6ec54ad 100644 --- a/src/TestConsoleApp/RootCommand/GreetCommand/Greet.cs +++ b/src/TestConsoleApp/RootCommand/GreetCommand/Greet.cs @@ -10,7 +10,7 @@ namespace TestConsoleApp.RootCommand.GreetCommand; public class Greet : Command, IUseCommandBuilder { - public Greet(ArgumentMapperRegistration mapperRegistration) : base(nameof(Greet).ToLower(), "Greets a person") + public Greet(SymbolMapperRegistration mapperRegistration) : base(nameof(Greet).ToLower(), "Greets a person") { this.UseCommandBuilder() .NewOption("prefix").Configure(o => @@ -29,9 +29,8 @@ public Greet(ArgumentMapperRegistration mapperRegistration) : base(nameof(Greet) o.Description = "An example option without DI mapping"; }).AddToCommand() .WithMapping(mapperRegistration) - .NewOption(x => x.Name).Configure(o => + .NewArgument(x => x.Name).Configure(o => { - o.Required = true; o.Description = "Name of the person to greet"; }).AddToCommand() .NewOption(x => x.Times).Configure(o => @@ -44,7 +43,7 @@ public Greet(ArgumentMapperRegistration mapperRegistration) : base(nameof(Greet) }).AddToCommand(); } - public static Greet CommandFactory(IServiceProvider sp, ArgumentMapperRegistration mapperRegistration) + public static Greet CommandFactory(IServiceProvider sp, SymbolMapperRegistration mapperRegistration) { return new Greet(mapperRegistration); } @@ -58,7 +57,7 @@ public class GreetOptions public class GreetHandler(IOptions options, ILogger logger) : AsynchronousCommandLineAction { - public async override Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) + public override async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) { await Task.CompletedTask; logger.LogDebug("greet"); diff --git a/src/TestConsoleApp/RootCommand/Root.cs b/src/TestConsoleApp/RootCommand/Root.cs index 226c191..8383cd2 100644 --- a/src/TestConsoleApp/RootCommand/Root.cs +++ b/src/TestConsoleApp/RootCommand/Root.cs @@ -12,7 +12,7 @@ namespace TestConsoleApp.RootCommand; internal class Root : System.CommandLine.RootCommand, IUseCommandBuilder { - public Root(ArgumentMapperRegistration mapperRegistration) : base("Sample ConsoleApp with DI and Serilog") + public Root(SymbolMapperRegistration mapperRegistration) : base("Sample ConsoleApp with DI and Serilog") { this.UseCommandBuilder().WithMapping(mapperRegistration) .NewOption(o => o.LogEventLevel).Configure(o => @@ -22,7 +22,7 @@ public Root(ArgumentMapperRegistration mapperRegistration) : base("Sample Consol }).AddToCommand(); } - public static Root CommandFactory(IServiceProvider sp, ArgumentMapperRegistration mapperRegistration) + public static Root CommandFactory(IServiceProvider sp, SymbolMapperRegistration mapperRegistration) { return new Root(mapperRegistration) { From c10a72343871e503da8f9f2d2ed0152549d8699f Mon Sep 17 00:00:00 2001 From: Dark Daskin Date: Wed, 31 Dec 2025 07:33:08 +0300 Subject: [PATCH 2/2] fixed an exception when multiple option types are mapped to a single command --- .../Builders/CommandSymbolBuilderWithMapping.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs b/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs index 2c534ad..3765f92 100644 --- a/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs +++ b/src/SystemCommandLine.Extensions/Builders/CommandSymbolBuilderWithMapping.cs @@ -11,7 +11,8 @@ protected void RegisterSymbolMapper(Symbol symbol) void symbolMapper(ParseResult parsedResult, object options) { - symbolValueMapper((TOptionHolder)options, parsedResult.GetValue(symbol.Name)); + if (options is TOptionHolder typedOptions) + symbolValueMapper(typedOptions, parsedResult.GetValue(symbol.Name)); } mapperRegistration(symbolMapper);