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<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() 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<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] 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<string>("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<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(); 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<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) { } 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 /// </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)); } } 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. /// </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) { } 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<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 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<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; 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; /// <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()); 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 /// </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> 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<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 }) {