Skip to content

Adds an optional case sensitive property to most tokens, defaulting to true #2284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
208 changes: 208 additions & 0 deletions src/System.CommandLine.Tests/CommandLineConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -33,6 +33,31 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_
.Be($"Duplicate alias '--dupe' found on command '{command.Name}'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_case_insensitive_sibling_option_aliases_on_the_root_command()
{
var option1 = new CliOption<string>("--dupe", false);
var option2 = new CliOption<string>("-y");
option2.Aliases.Add("--Dupe");

var command = new CliRootCommand()
{
option1,
option2
};

var config = new CliConfiguration(command);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be($"Duplicate alias '--dupe' found on command '{command.Name}'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_a_subcommand()
{
@@ -60,6 +85,33 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_
.Should()
.Be("Duplicate alias '--dupe' found on command 'subcommand'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_case_insensitive_sibling_option_aliases_on_a_subcommand()
{
var option1 = new CliOption<string>("--dupe", false);
var option2 = new CliOption<string>("--ok");
option2.Aliases.Add("--Dupe");

var command = new CliRootCommand
{
new CliCommand("subcommand")
{
option1,
option2
}
};

var config = new CliConfiguration(command);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be("Duplicate alias '--dupe' found on command 'subcommand'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_the_root_command()
@@ -85,6 +137,30 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia
.Should()
.Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_case_insensitive_sibling_subcommand_aliases_on_the_root_command()
{
var command1 = new CliCommand("dupe", caseSensitive: false);
var command2 = new CliCommand("not-a-dupe");
command2.Aliases.Add("Dupe");

var rootCommand = new CliRootCommand
{
command1,
command2
};

var config = new CliConfiguration(rootCommand);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_a_subcommand()
@@ -109,6 +185,29 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia
.Should()
.Be("Duplicate alias 'dupe' found on command 'subcommand'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_case_insensitive_sibling_subcommand_aliases_on_a_subcommand()
{
var command = new CliRootCommand
{
new CliCommand("subcommand")
{
new CliCommand("dupe", caseSensitive: false),
new CliCommand("not-a-dupe") { Aliases = { "Dupe" } }
}
};

var config = new CliConfiguration(command);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be("Duplicate alias 'dupe' found on command 'subcommand'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_the_root_command()
@@ -134,6 +233,30 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_
.Should()
.Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_case_insensitive_sibling_command_and_option_aliases_collide_on_the_root_command()
{
var option = new CliOption<string>("dupe", caseSensitive: false);
var command = new CliCommand("not-a-dupe");
command.Aliases.Add("Dupe");

var rootCommand = new CliRootCommand
{
option,
command
};

var config = new CliConfiguration(rootCommand);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_a_subcommand()
@@ -162,6 +285,33 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_
.Should()
.Be("Duplicate alias 'dupe' found on command 'subcommand'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_case_insensitive_sibling_command_and_option_aliases_collide_on_a_subcommand()
{
var option = new CliOption<string>("dupe", caseSensitive: false);
var command = new CliCommand("not-a-dupe");
command.Aliases.Add("Dupe");

var rootCommand = new CliRootCommand
{
new CliCommand("subcommand")
{
option,
command
}
};

var config = new CliConfiguration(rootCommand);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be("Duplicate alias 'dupe' found on command 'subcommand'.");
}

[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_aliases_on_the_root_command()
@@ -185,6 +335,28 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_a
.Should()
.Be($"Duplicate alias '--dupe' found on command '{command.Name}'.");
}
[Fact]
public void ThrowIfInvalid_throws_if_there_are_duplicate_case_insensitive_sibling_global_option_aliases_on_the_root_command()
{
var option1 = new CliOption<string>("--dupe", caseSensitive: false) { Recursive = true };
var option2 = new CliOption<string>("-y") { Recursive = true };
option2.Aliases.Add("--Dupe");

var command = new CliRootCommand();
command.Options.Add(option1);
command.Options.Add(option2);

var config = new CliConfiguration(command);

var validate = () => config.ThrowIfInvalid();

validate.Should()
.Throw<CliConfigurationException>()
.Which
.Message
.Should()
.Be($"Duplicate alias '--dupe' found on command '{command.Name}'.");
}

[Fact]
public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_local_option_alias()
@@ -204,6 +376,24 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_

validate.Should().NotThrow();
}
[Fact]
public void ThrowIfInvalid_does_not_throw_if_case_insensitive_global_option_alias_is_the_same_as_local_option_alias()
{
var rootCommand = new CliRootCommand
{
new CliCommand("subcommand")
{
new CliOption<string>("--dupe")
}
};
rootCommand.Options.Add(new CliOption<string>("--Dupe", caseSensitive: false) { Recursive = true });

var config = new CliConfiguration(rootCommand);

var validate = () => config.ThrowIfInvalid();

validate.Should().NotThrow();
}

[Fact]
public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_subcommand_alias()
@@ -223,6 +413,24 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_

validate.Should().NotThrow();
}
[Fact]
public void ThrowIfInvalid_does_not_throw_if_case_insensitive_global_option_alias_is_the_same_as_subcommand_alias()
{
var rootCommand = new CliRootCommand
{
new CliCommand("subcommand")
{
new CliCommand("--dupe")
}
};
rootCommand.Options.Add(new CliOption<string>("--Dupe", caseSensitive: false) { Recursive = true });

var config = new CliConfiguration(rootCommand);

var validate = () => config.ThrowIfInvalid();

validate.Should().NotThrow();
}

[Fact]
public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent()
132 changes: 127 additions & 5 deletions src/System.CommandLine.Tests/CommandTests.cs
Original file line number Diff line number Diff line change
@@ -10,7 +10,10 @@ namespace System.CommandLine.Tests
{
public class CommandTests
{
private const string caseSensitiveInvoke = "outer inner --option argument1";
private const string caseInsensitiveInvoke = "Outer Inner --Option argument1";
private readonly CliCommand _outerCommand;
private readonly CliCommand _outerCommandInsensitive;

public CommandTests()
{
@@ -21,12 +24,31 @@ public CommandTests()
new CliOption<string>("--option")
}
};
_outerCommandInsensitive = new CliCommand("outer", caseSensitive: false)
{
new CliCommand("inner", caseSensitive: false)
{
new CliOption<string>("--option", caseSensitive: false)
}
};
}

[Fact]
public void Outer_command_is_identified_correctly_by_RootCommand()
{
var result = _outerCommand.Parse("outer inner --option argument1");
var result = _outerCommand.Parse(caseSensitiveInvoke);

result
.RootCommandResult
.Command
.Name
.Should()
.Be("outer");
}
[Fact]
public void Outer_command_is_identified_correctly_by_RootCommand_while_case_insensitive()
{
var result = _outerCommandInsensitive.Parse(caseInsensitiveInvoke);

result
.RootCommandResult
@@ -39,7 +61,23 @@ public void Outer_command_is_identified_correctly_by_RootCommand()
[Fact]
public void Outer_command_is_identified_correctly_by_Parent_property()
{
var result = _outerCommand.Parse("outer inner --option argument1");
var result = _outerCommand.Parse(caseSensitiveInvoke);

result
.CommandResult
.Parent
.Should()
.BeOfType<CommandResult>()
.Which
.Command
.Name
.Should()
.Be("outer");
}
[Fact]
public void Outer_command_is_identified_correctly_by_Parent_property_while_case_insensitive()
{
var result = _outerCommandInsensitive.Parse(caseInsensitiveInvoke);

result
.CommandResult
@@ -56,7 +94,7 @@ public void Outer_command_is_identified_correctly_by_Parent_property()
[Fact]
public void Inner_command_is_identified_correctly()
{
var result = _outerCommand.Parse("outer inner --option argument1");
var result = _outerCommand.Parse(caseSensitiveInvoke);

result.CommandResult
.Should()
@@ -67,11 +105,73 @@ public void Inner_command_is_identified_correctly()
.Should()
.Be("inner");
}
[Fact]
public void Inner_command_is_identified_correctly_while_case_insensitive()
{
var result = _outerCommandInsensitive.Parse(caseInsensitiveInvoke);

result.CommandResult
.Should()
.BeOfType<CommandResult>()
.Which
.Command
.Name
.Should()
.Be("inner");
}
[Fact]
public void Case_sensitive_inner_child_remains_case_sensitive()
{
var mixedCommand = new CliCommand("outer", caseSensitive: false)
{
new CliCommand("inner", caseSensitive: true)
{
new CliOption<string>("--option", caseSensitive: false)
}
};
var result = mixedCommand.Parse(caseInsensitiveInvoke);
result.Errors.Should().NotBeEmpty();
}
public void Case_insensitive_inner_child_is_identified_correctly_while_outer_is_case_sensitive()
{
var mixedCommand = new CliCommand("outer")
{
new CliCommand("inner", caseSensitive: false)
{
new CliOption<string>("--option", caseSensitive: false)
}
};
var result = mixedCommand.Parse("outer Inner --Option argument1");
result.CommandResult
.Should()
.BeOfType<CommandResult>()
.Which
.Command
.Name
.Should()
.Be("inner");
}

[Fact]
public void Inner_command_option_is_identified_correctly()
{
var result = _outerCommand.Parse("outer inner --option argument1");
var result = _outerCommand.Parse(caseSensitiveInvoke);

result.CommandResult
.Children
.ElementAt(0)
.Should()
.BeOfType<OptionResult>()
.Which
.Option
.Name
.Should()
.Be("--option");
}
[Fact]
public void Inner_command_option_is_identified_correctly_while_case_insensitive()
{
var result = _outerCommandInsensitive.Parse(caseInsensitiveInvoke);

result.CommandResult
.Children
@@ -88,7 +188,20 @@ public void Inner_command_option_is_identified_correctly()
[Fact]
public void Inner_command_option_argument_is_identified_correctly()
{
var result = _outerCommand.Parse("outer inner --option argument1");
var result = _outerCommand.Parse(caseSensitiveInvoke);

result.CommandResult
.Children
.ElementAt(0)
.Tokens
.Select(t => t.Value)
.Should()
.BeEquivalentTo("argument1");
}
[Fact]
public void Inner_command_option_argument_is_identified_correctly_while_case_insensitive()
{
var result = _outerCommandInsensitive.Parse(caseInsensitiveInvoke);

result.CommandResult
.Children
@@ -137,6 +250,15 @@ public void Aliases_is_aware_of_added_alias()

command.Aliases.Should().Contain("added");
}
[Fact]
public void Aliases_is_aware_of_added_alias_while_case_insensitive()
{
var command = new CliCommand("original", caseSensitive: false);

command.Aliases.Add("Added");

command.Aliases.Should().Contain("added");
}


[Theory]
7 changes: 7 additions & 0 deletions src/System.CommandLine.Tests/OptionTests.cs
Original file line number Diff line number Diff line change
@@ -90,6 +90,13 @@ public void Option_aliases_are_case_sensitive()

option.Aliases.Contains("O").Should().BeFalse();
}
[Fact]
public void Option_aliases_are_case_insensitive_while_option_is_case_insensitive()
{
var option = new CliOption<string>("name", caseSensitive: false, "o");

option.Aliases.Contains("O").Should().BeTrue();
}

[Fact]
public void Aliases_contains_prefixed_short_value()
7 changes: 4 additions & 3 deletions src/System.CommandLine/AliasSet.cs
Original file line number Diff line number Diff line change
@@ -8,16 +8,16 @@ internal sealed class AliasSet : ICollection<string>
{
private readonly HashSet<string> _aliases;

internal AliasSet() => _aliases = new(StringComparer.Ordinal);
internal AliasSet(bool caseSensitive) => _aliases = new(caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);

internal AliasSet(string[] aliases)
internal AliasSet(string[] aliases, bool caseSensitive)
{
foreach (string alias in aliases)
{
CliSymbol.ThrowIfEmptyOrWithWhitespaces(alias, nameof(alias));
}

_aliases = new(aliases, StringComparer.Ordinal);
_aliases = new(aliases, caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
}

public int Count => _aliases.Count;
@@ -29,6 +29,7 @@ public void Add(string item)

internal bool Overlaps(AliasSet other) => _aliases.Overlaps(other._aliases);


// a struct based enumerator for avoiding allocations
public HashSet<string>.Enumerator GetEnumerator() => _aliases.GetEnumerator();

2 changes: 1 addition & 1 deletion src/System.CommandLine/CliArgument.cs
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ public abstract class CliArgument : CliSymbol
private List<Func<CompletionContext, IEnumerable<CompletionItem>>>? _completionSources = null;
private List<Action<ArgumentResult>>? _validators = null;

private protected CliArgument(string name) : base(name, allowWhitespace: true)
private protected CliArgument(string name, bool caseSensitive = true) : base(name, allowWhitespace: true, caseSensitive: caseSensitive)
{
}

8 changes: 5 additions & 3 deletions src/System.CommandLine/CliCommand.cs
Original file line number Diff line number Diff line change
@@ -35,7 +35,8 @@ public class CliCommand : CliSymbol, IEnumerable
/// </summary>
/// <param name="name">The name of the command.</param>
/// <param name="description">The description of the command, shown in help.</param>
public CliCommand(string name, string? description = null) : base(name)
/// <param name="caseSensitive">Whether the command is case sensitive.</param>
public CliCommand(string name, string? description = null, bool caseSensitive = true) : base(name, caseSensitive: caseSensitive)
=> Description = description;

/// <summary>
@@ -89,7 +90,7 @@ public IEnumerable<CliSymbol> Children
/// Gets the unique set of strings that can be used on the command line to specify the command.
/// </summary>
/// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Command.</remarks>
public ICollection<string> Aliases => _aliases ??= new();
public ICollection<string> Aliases => _aliases ??= new(CaseSensitive);

/// <summary>
/// Gets or sets the <see cref="CliAction"/> for the Command. The handler represents the action
@@ -308,6 +309,7 @@ void AddCompletionsFor(CliSymbol identifier, AliasSet? aliases)
}

internal bool EqualsNameOrAlias(string name)
=> Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name));
=> Name.Equals(name, CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)
|| (_aliases is not null && _aliases.Contains(name, CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase));
}
}
6 changes: 3 additions & 3 deletions src/System.CommandLine/CliConfiguration.cs
Original file line number Diff line number Diff line change
@@ -176,12 +176,12 @@ static void ThrowIfInvalid(CliCommand command)
{
CliSymbol symbol2 = GetChild(j, command, out AliasSet? aliases2);

if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal)
|| (aliases1 is not null && aliases1.Contains(symbol2.Name)))
if (symbol1.Name.Equals(symbol2.Name, symbol1.CaseSensitive && symbol2.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)
|| (aliases1 is not null && aliases1.Contains(symbol2.Name, symbol1.CaseSensitive && symbol2.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase)))
{
throw new CliConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'.");
}
else if (aliases2 is not null && aliases2.Contains(symbol1.Name))
else if (aliases2 is not null && aliases2.Contains(symbol1.Name, symbol1.CaseSensitive && symbol2.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase))
{
throw new CliConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'.");
}
5 changes: 3 additions & 2 deletions src/System.CommandLine/CliDirective.cs
Original file line number Diff line number Diff line change
@@ -22,8 +22,9 @@ public class CliDirective : CliSymbol
/// Initializes a new instance of the Directive class.
/// </summary>
/// <param name="name">The name of the directive. It can't contain whitespaces.</param>
public CliDirective(string name)
: base(name)
/// <param name="caseSensitive">Whether the directive is case sensitive.</param>
public CliDirective(string name, bool caseSensitive = true)
: base(name, caseSensitive: caseSensitive)
{
}

6 changes: 3 additions & 3 deletions src/System.CommandLine/CliOption.cs
Original file line number Diff line number Diff line change
@@ -17,11 +17,11 @@ public abstract class CliOption : CliSymbol
internal AliasSet? _aliases;
private List<Action<OptionResult>>? _validators;

private protected CliOption(string name, string[] aliases) : base(name)
private protected CliOption(string name, string[] aliases, bool caseSensitive = true) : base(name, caseSensitive: caseSensitive)
{
if (aliases is { Length: > 0 })
{
_aliases = new(aliases);
_aliases = new(aliases, caseSensitive);
}
}

@@ -102,7 +102,7 @@ internal virtual bool Greedy
/// Gets the unique set of strings that can be used on the command line to specify the Option.
/// </summary>
/// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Option.</remarks>
public ICollection<string> Aliases => _aliases ??= new();
public ICollection<string> Aliases => _aliases ??= new(CaseSensitive);

/// <summary>
/// Gets or sets the <see cref="CliAction"/> for the Option. The handler represents the action
14 changes: 12 additions & 2 deletions src/System.CommandLine/CliOption{T}.cs
Original file line number Diff line number Diff line change
@@ -20,9 +20,19 @@ public CliOption(string name, params string[] aliases)
: this(name, aliases, new CliArgument<T>(name))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CliOption"/> class.
/// </summary>
/// <param name="name">The name of the option. It's used for parsing, displaying Help and creating parse errors.</param>>
/// <param name="caseSensitive">Whether the option is case sensitive.</param>
/// <param name="aliases">Optional aliases. Used for parsing, suggestions and displayed in Help.</param>
public CliOption(string name, bool caseSensitive, params string[] aliases)
: this(name, aliases, new CliArgument<T>(name), caseSensitive)
{
}

private protected CliOption(string name, string[] aliases, CliArgument<T> argument)
: base(name, aliases)
private protected CliOption(string name, string[] aliases, CliArgument<T> argument, bool caseSensitive = true)
: base(name, aliases, caseSensitive)
{
argument.AddParent(this);
_argument = argument;
3 changes: 2 additions & 1 deletion src/System.CommandLine/CliRootCommand.cs
Original file line number Diff line number Diff line change
@@ -25,7 +25,8 @@ public class CliRootCommand : CliCommand
private static string? _executableVersion;

/// <param name="description">The description of the command, shown in help.</param>
public CliRootCommand(string description = "") : base(ExecutableName, description)
/// <param name="caseSensitive">Whether the option is case sensitive.</param>
public CliRootCommand(string description = "", bool caseSensitive = true) : base(ExecutableName, description, caseSensitive)
{
Options.Add(new HelpOption());
Options.Add(new VersionOption());
5 changes: 4 additions & 1 deletion src/System.CommandLine/CliSymbol.cs
Original file line number Diff line number Diff line change
@@ -12,9 +12,10 @@ namespace System.CommandLine
/// </summary>
public abstract class CliSymbol
{
private protected CliSymbol(string name, bool allowWhitespace = false)
private protected CliSymbol(string name, bool allowWhitespace = false, bool caseSensitive = true)
{
Name = ThrowIfEmptyOrWithWhitespaces(name, nameof(name), allowWhitespace);
CaseSensitive = caseSensitive;
}

/// <summary>
@@ -54,6 +55,8 @@ internal void AddParent(CliSymbol symbol)
/// </summary>
public bool Hidden { get; set; }

internal bool CaseSensitive { get; set; } = true;

/// <summary>
/// Gets the parent symbols.
/// </summary>
20 changes: 19 additions & 1 deletion src/System.CommandLine/Parsing/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -155,10 +155,28 @@ internal static void Tokenize(
switch (token.Type)
{
case CliTokenType.Option:
if (token?.Symbol?.CaseSensitive ?? false)
{
// If the option is case sensitive, we need to make sure that the match was sensitive
if(!arg.Equals(token.Value, StringComparison.Ordinal))
{
// it doesn't match, so we need to keep going
break;
}
}
tokenList.Add(Option(arg, (CliOption)token.Symbol!));
break;

case CliTokenType.Command:
if (token?.Symbol?.CaseSensitive ?? false)
{
// If the option is case sensitive, we need to make sure that the match was sensitive
if (!arg.Equals(token.Value, StringComparison.Ordinal))
{
// it doesn't match, so we need to keep going
break;
}
}
CliCommand cmd = (CliCommand)token.Symbol!;
if (cmd != currentCommand)
{
@@ -412,7 +430,7 @@ static IEnumerable<string> SplitLine(string line)

private static Dictionary<string, CliToken> ValidTokens(this CliCommand command)
{
Dictionary<string, CliToken> tokens = new(StringComparer.Ordinal);
Dictionary<string, CliToken> tokens = new(command.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);

if (command is CliRootCommand { Directives: IList<CliDirective> directives })
{