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