diff --git a/src/Serde.CmdLine/Deserializer.DeserializeType.cs b/src/Serde.CmdLine/Deserializer.DeserializeType.cs index 396c3d5..5a1b53d 100644 --- a/src/Serde.CmdLine/Deserializer.DeserializeType.cs +++ b/src/Serde.CmdLine/Deserializer.DeserializeType.cs @@ -55,12 +55,19 @@ private int TryReadIndex(ISerdeInfo serdeInfo) continue; } - var (fieldIndex, incArgs) = CheckFields(arg); + var (fieldIndex, fieldKind) = CheckFields(arg); if (fieldIndex >= 0) { - if (incArgs) + // Increment indices based on field type + switch (fieldKind) { - argIndex++; + case FieldKind.Option: + case FieldKind.SubCommand: + argIndex++; + break; + case FieldKind.Parameter: + _deserializer._paramIndex++; + break; } return fieldIndex; } @@ -156,13 +163,13 @@ int ITypeDeserializer.TryReadIndex(ISerdeInfo info) return null; } - private (int fieldIndex, bool incArgs) CheckFields(string arg) + private (int fieldIndex, FieldKind fieldKind) CheckFields(string arg) { var cmd = _command; if (arg.StartsWith('-')) { var fieldIndex = CheckOptions(cmd, arg)?.FieldIndex ?? -1; - return (fieldIndex, fieldIndex >= 0); + return (fieldIndex, fieldIndex >= 0 ? FieldKind.Option : FieldKind.None); } // Check for command group matches @@ -170,7 +177,7 @@ int ITypeDeserializer.TryReadIndex(ISerdeInfo info) { if (arg == subCmd.Name) { - return (subCmd.FieldIndex, true); + return (subCmd.FieldIndex, FieldKind.SubCommand); } } @@ -180,7 +187,7 @@ int ITypeDeserializer.TryReadIndex(ISerdeInfo info) { if (name == arg) { - return (cmdGroup.FieldIndex, false); + return (cmdGroup.FieldIndex, FieldKind.CommandGroup); } } @@ -193,10 +200,10 @@ int ITypeDeserializer.TryReadIndex(ISerdeInfo info) // Parameters are positional, so we check the current param index if (_deserializer._paramIndex == param.Ordinal) { - return (param.FieldIndex, false); + return (param.FieldIndex, FieldKind.Parameter); } } - return (-1, false); + return (-1, FieldKind.None); } public static Command ParseCommand(ISerdeInfo serdeInfo) diff --git a/src/Serde.CmdLine/OptionTypes.cs b/src/Serde.CmdLine/OptionTypes.cs index d21a934..3fcc277 100644 --- a/src/Serde.CmdLine/OptionTypes.cs +++ b/src/Serde.CmdLine/OptionTypes.cs @@ -3,6 +3,30 @@ namespace Serde.CmdLine; +internal enum FieldKind +{ + /// + /// No field matched + /// + None, + /// + /// An option field matched (requires incrementing arg index) + /// + Option, + /// + /// A subcommand field matched (requires incrementing arg index) + /// + SubCommand, + /// + /// A command group field matched (does not increment arg index) + /// + CommandGroup, + /// + /// A parameter field matched (requires incrementing param index) + /// + Parameter +} + internal record struct Option( ImmutableArray FlagNames, int FieldIndex, diff --git a/test/Serde.CmdLine.Test/SubCommandTests.cs b/test/Serde.CmdLine.Test/SubCommandTests.cs index dae790b..b760fe5 100644 --- a/test/Serde.CmdLine.Test/SubCommandTests.cs +++ b/test/Serde.CmdLine.Test/SubCommandTests.cs @@ -191,4 +191,47 @@ public sealed partial record FirstCommand : SubCommand [Command("second")] public sealed partial record SecondCommand : SubCommand; } + + /// + /// Test to verify that two command parameters in a nested subcommand are parsed correctly. + /// This test is designed to reproduce a bug where the second parameter is parsed as a + /// duplicate of the first parameter due to _paramIndex not being incremented. + /// + [Fact] + public void NestedSubCommandWithTwoParameters() + { + string[] testArgs = [ "copy", "source.txt", "dest.txt" ]; + var cmd = CmdLine.ParseRawWithHelp(testArgs).Unwrap(); + Assert.Equal(new CommandWithParams + { + SubCommandWithParams = new SubCommandWithParams.CopyCommand + { + Source = "source.txt", + Destination = "dest.txt" + } + }, cmd); + } + + [GenerateDeserialize] + private partial record CommandWithParams + { + [CommandGroup("command")] + public SubCommandWithParams? SubCommandWithParams { get; init; } + } + + [GenerateDeserialize] + private abstract partial record SubCommandWithParams + { + private SubCommandWithParams() { } + + [Command("copy")] + public sealed partial record CopyCommand : SubCommandWithParams + { + [CommandParameter(0, "source")] + public string? Source { get; init; } + + [CommandParameter(1, "destination")] + public string? Destination { get; init; } + } + } } \ No newline at end of file