Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/Serde.CmdLine/Deserializer.DeserializeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -156,21 +163,21 @@ 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
foreach (var subCmd in cmd.SubCommands)
{
if (arg == subCmd.Name)
{
return (subCmd.FieldIndex, true);
return (subCmd.FieldIndex, FieldKind.SubCommand);
}
}

Expand All @@ -180,7 +187,7 @@ int ITypeDeserializer.TryReadIndex(ISerdeInfo info)
{
if (name == arg)
{
return (cmdGroup.FieldIndex, false);
return (cmdGroup.FieldIndex, FieldKind.CommandGroup);
}
}

Expand All @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions src/Serde.CmdLine/OptionTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@

namespace Serde.CmdLine;

internal enum FieldKind
{
/// <summary>
/// No field matched
/// </summary>
None,
/// <summary>
/// An option field matched (requires incrementing arg index)
/// </summary>
Option,
/// <summary>
/// A subcommand field matched (requires incrementing arg index)
/// </summary>
SubCommand,
/// <summary>
/// A command group field matched (does not increment arg index)
/// </summary>
CommandGroup,
/// <summary>
/// A parameter field matched (requires incrementing param index)
/// </summary>
Parameter
}

internal record struct Option(
ImmutableArray<string> FlagNames,
int FieldIndex,
Expand Down
43 changes: 43 additions & 0 deletions test/Serde.CmdLine.Test/SubCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,47 @@ public sealed partial record FirstCommand : SubCommand
[Command("second")]
public sealed partial record SecondCommand : SubCommand;
}

/// <summary>
/// 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.
/// </summary>
[Fact]
public void NestedSubCommandWithTwoParameters()
{
string[] testArgs = [ "copy", "source.txt", "dest.txt" ];
var cmd = CmdLine.ParseRawWithHelp<CommandWithParams>(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; }
}
}
}