diff --git a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs index bebf79dd4e..1a6abf222f 100644 --- a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs +++ b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs @@ -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("--dupe", false); + var option2 = new CliOption("-y"); + option2.Aliases.Add("--Dupe"); + + var command = new CliRootCommand() + { + option1, + option2 + }; + + var config = new CliConfiguration(command); + + var validate = () => config.ThrowIfInvalid(); + + validate.Should() + .Throw() + .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("--dupe", false); + var option2 = new CliOption("--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() + .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() + .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() + .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("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() + .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("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() + .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("--dupe", caseSensitive: false) { Recursive = true }; + var option2 = new CliOption("-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() + .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("--dupe") + } + }; + rootCommand.Options.Add(new CliOption("--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("--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() diff --git a/src/System.CommandLine.Tests/CommandTests.cs b/src/System.CommandLine.Tests/CommandTests.cs index 8e2157932d..e9696771ef 100644 --- a/src/System.CommandLine.Tests/CommandTests.cs +++ b/src/System.CommandLine.Tests/CommandTests.cs @@ -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("--option") } }; + _outerCommandInsensitive = new CliCommand("outer", caseSensitive: false) + { + new CliCommand("inner", caseSensitive: false) + { + new CliOption("--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() + .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() + .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("--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("--option", caseSensitive: false) + } + }; + var result = mixedCommand.Parse("outer Inner --Option argument1"); + result.CommandResult + .Should() + .BeOfType() + .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() + .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] diff --git a/src/System.CommandLine.Tests/OptionTests.cs b/src/System.CommandLine.Tests/OptionTests.cs index 193448b075..1d1ad600e8 100644 --- a/src/System.CommandLine.Tests/OptionTests.cs +++ b/src/System.CommandLine.Tests/OptionTests.cs @@ -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("name", caseSensitive: false, "o"); + + option.Aliases.Contains("O").Should().BeTrue(); + } [Fact] public void Aliases_contains_prefixed_short_value() diff --git a/src/System.CommandLine/AliasSet.cs b/src/System.CommandLine/AliasSet.cs index 6007007843..ca7e94177f 100644 --- a/src/System.CommandLine/AliasSet.cs +++ b/src/System.CommandLine/AliasSet.cs @@ -8,16 +8,16 @@ internal sealed class AliasSet : ICollection { private readonly HashSet _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.Enumerator GetEnumerator() => _aliases.GetEnumerator(); diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index aa453bfd72..b38bab439d 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -19,7 +19,7 @@ public abstract class CliArgument : CliSymbol private List>>? _completionSources = null; private List>? _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) { } diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index e101bf3948..ca8aebecef 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -35,7 +35,8 @@ public class CliCommand : CliSymbol, IEnumerable /// /// The name of the command. /// The description of the command, shown in help. - public CliCommand(string name, string? description = null) : base(name) + /// Whether the command is case sensitive. + public CliCommand(string name, string? description = null, bool caseSensitive = true) : base(name, caseSensitive: caseSensitive) => Description = description; /// @@ -89,7 +90,7 @@ public IEnumerable Children /// Gets the unique set of strings that can be used on the command line to specify the command. /// /// The collection does not contain the of the Command. - public ICollection Aliases => _aliases ??= new(); + public ICollection Aliases => _aliases ??= new(CaseSensitive); /// /// Gets or sets the 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)); } } diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index dc02b4e512..d5a316c320 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -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}'."); } diff --git a/src/System.CommandLine/CliDirective.cs b/src/System.CommandLine/CliDirective.cs index cb7930f5fe..7ad50a712a 100644 --- a/src/System.CommandLine/CliDirective.cs +++ b/src/System.CommandLine/CliDirective.cs @@ -22,8 +22,9 @@ public class CliDirective : CliSymbol /// Initializes a new instance of the Directive class. /// /// The name of the directive. It can't contain whitespaces. - public CliDirective(string name) - : base(name) + /// Whether the directive is case sensitive. + public CliDirective(string name, bool caseSensitive = true) + : base(name, caseSensitive: caseSensitive) { } diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index fd204a8be4..43b47ac092 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -17,11 +17,11 @@ public abstract class CliOption : CliSymbol internal AliasSet? _aliases; private List>? _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. /// /// The collection does not contain the of the Option. - public ICollection Aliases => _aliases ??= new(); + public ICollection Aliases => _aliases ??= new(CaseSensitive); /// /// Gets or sets the for the Option. The handler represents the action diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 0a9e857578..d8176ae5db 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -20,9 +20,19 @@ public CliOption(string name, params string[] aliases) : this(name, aliases, new CliArgument(name)) { } + /// + /// Initializes a new instance of the class. + /// + /// The name of the option. It's used for parsing, displaying Help and creating parse errors.> + /// Whether the option is case sensitive. + /// Optional aliases. Used for parsing, suggestions and displayed in Help. + public CliOption(string name, bool caseSensitive, params string[] aliases) + : this(name, aliases, new CliArgument(name), caseSensitive) + { + } - private protected CliOption(string name, string[] aliases, CliArgument argument) - : base(name, aliases) + private protected CliOption(string name, string[] aliases, CliArgument argument, bool caseSensitive = true) + : base(name, aliases, caseSensitive) { argument.AddParent(this); _argument = argument; diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index 7c150b2440..4be35c9106 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -25,7 +25,8 @@ public class CliRootCommand : CliCommand private static string? _executableVersion; /// The description of the command, shown in help. - public CliRootCommand(string description = "") : base(ExecutableName, description) + /// Whether the option is case sensitive. + public CliRootCommand(string description = "", bool caseSensitive = true) : base(ExecutableName, description, caseSensitive) { Options.Add(new HelpOption()); Options.Add(new VersionOption()); diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index 35ccd1887e..5487f9d4dd 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -12,9 +12,10 @@ namespace System.CommandLine /// 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; } /// @@ -54,6 +55,8 @@ internal void AddParent(CliSymbol symbol) /// public bool Hidden { get; set; } + internal bool CaseSensitive { get; set; } = true; + /// /// Gets the parent symbols. /// diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 169070c5f7..e5c76499f7 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -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 SplitLine(string line) private static Dictionary ValidTokens(this CliCommand command) { - Dictionary tokens = new(StringComparer.Ordinal); + Dictionary tokens = new(command.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); if (command is CliRootCommand { Directives: IList directives }) {