diff --git a/src/Spectre.Console.Cli/CommandParseException.cs b/src/Spectre.Console.Cli/CommandParseException.cs index 2520da8..0f4dd1b 100644 --- a/src/Spectre.Console.Cli/CommandParseException.cs +++ b/src/Spectre.Console.Cli/CommandParseException.cs @@ -91,11 +91,6 @@ public sealed class CommandParseException : CommandRuntimeException return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", "Invalid character."); } - internal static CommandParseException UnterminatedQuote(string input, CommandTreeToken token) - { - return CommandLineParseExceptionFactory.Create(input, token, $"Encountered unterminated quoted string '{token.Value}'.", "Did you forget the closing quotation mark?"); - } - internal static CommandParseException UnknownCommand(CommandModel model, CommandTree? node, IEnumerable args, CommandTreeToken token) { var suggestion = CommandSuggestor.Suggest(model, node?.Command, token.Value); diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs index 8801ed0..197f423 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs @@ -308,19 +308,35 @@ internal class CommandTreeParser { // Is this a command? if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null) - { - if (parameter != null) - { - if (parameter.ParameterKind == ParameterKind.Flag) - { - if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) - { - // Flags cannot be assigned a value. - throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); - } - } - - value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + { + if (parameter != null) + { + if (parameter.ParameterKind == ParameterKind.Flag) + { + if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) + { + if (!valueToken.HadSeparator) + { + // Do nothing + // - assume valueToken is unrelated to the flag parameter (ie. we've parsed it unnecessarily) + // - rely on the "No value?" code below to set the flag to its default value + // - valueToken will be handled on the next pass of the parser + } + else + { + // Flags cannot be assigned a value. + throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); + } + } + else + { + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + } + } + else + { + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + } } else { diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs index 189792e..8f30072 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs @@ -6,7 +6,12 @@ internal sealed class CommandTreeToken public int Position { get; } public string Value { get; } public string Representation { get; } - public bool IsGrouped { get; set; } + public bool IsGrouped { get; set; } + + /// + /// Gets or sets a value indicating whether a separater was encountered immediately before the . + /// + public bool HadSeparator { get; set; } public enum Kind { diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs index c37303b..9006928 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs @@ -29,7 +29,14 @@ internal static class CommandTreeTokenizer var context = new CommandTreeTokenizerContext(); foreach (var arg in args) - { + { + if (string.IsNullOrEmpty(arg)) + { + // Null strings in the args array are still represented as tokens + tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty)); + continue; + } + var start = position; var reader = new TextBuffer(previousReader, arg); @@ -48,39 +55,30 @@ internal static class CommandTreeTokenizer } private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List tokens) - { - while (reader.Peek() != -1) - { - if (reader.ReachedEnd) - { - position += reader.Position - start; - break; - } - - var character = reader.Peek(); - - // Eat whitespace - if (char.IsWhiteSpace(character)) - { - reader.Consume(); - continue; - } - - if (character == '-') - { - // Option - tokens.AddRange(ScanOptions(context, reader)); - } - else - { - // Command or argument - tokens.Add(ScanString(context, reader)); - } - - // Flush remaining tokens - context.FlushRemaining(); - } - + { + if (!reader.ReachedEnd && reader.Peek() == '-') + { + // Option + tokens.AddRange(ScanOptions(context, reader)); + } + else + { + // Command or argument + while (reader.Peek() != -1) + { + if (reader.ReachedEnd) + { + position += reader.Position - start; + break; + } + + tokens.Add(ScanString(context, reader)); + + // Flush remaining tokens + context.FlushRemaining(); + } + } + return position; } @@ -89,15 +87,6 @@ internal static class CommandTreeTokenizer TextBuffer reader, char[]? stop = null) { - if (reader.TryPeek(out var character)) - { - // Is this a quoted string? - if (character == '\"') - { - return ScanQuotedString(context, reader); - } - } - var position = reader.Position; var builder = new StringBuilder(); while (!reader.ReachedEnd) @@ -113,48 +102,8 @@ internal static class CommandTreeTokenizer builder.Append(current); } - var value = builder.ToString(); - return new CommandTreeToken(CommandTreeToken.Kind.String, position, value.Trim(), value); - } - - private static CommandTreeToken ScanQuotedString(CommandTreeTokenizerContext context, TextBuffer reader) - { - var position = reader.Position; - - context.FlushRemaining(); - reader.Consume('\"'); - - var builder = new StringBuilder(); - var terminated = false; - while (!reader.ReachedEnd) - { - var character = reader.Peek(); - if (character == '\"') - { - terminated = true; - reader.Read(); - break; - } - - builder.Append(reader.Read()); - } - - if (!terminated) - { - var unterminatedQuote = builder.ToString(); - var token = new CommandTreeToken(CommandTreeToken.Kind.String, position, unterminatedQuote, $"\"{unterminatedQuote}"); - throw CommandParseException.UnterminatedQuote(reader.Original, token); - } - - var quotedString = builder.ToString(); - - // Add to the context - context.AddRemaining(quotedString); - - return new CommandTreeToken( - CommandTreeToken.Kind.String, - position, quotedString, - quotedString); + var value = builder.ToString(); + return new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value); } private static IEnumerable ScanOptions(CommandTreeTokenizerContext context, TextBuffer reader) @@ -166,7 +115,7 @@ internal static class CommandTreeTokenizer reader.Consume('-'); context.AddRemaining('-'); - if (!reader.TryPeek(out var character)) + if (!reader.TryPeek(out var character) || character == ' ') { var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, "-", "-"); throw CommandParseException.OptionHasNoName(reader.Original, token); @@ -200,8 +149,10 @@ internal static class CommandTreeTokenizer var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "="); throw CommandParseException.OptionValueWasExpected(reader.Original, token); } - - result.Add(ScanString(context, reader)); + + var tokenValue = ScanString(context, reader); + tokenValue.HadSeparator = true; + result.Add(tokenValue); } } @@ -235,12 +186,38 @@ internal static class CommandTreeTokenizer ? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}") : new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value)); } - else + else if (result.Count == 0 && char.IsDigit(current)) { + // We require short options to be named with letters. Short options that start with a number + // ("-1", "-2ab", "-3..7") may actually mean values (either for options or arguments) and will + // be tokenized as strings. This block handles parsing those cases, but we only allow this + // when the digit is the first character in the token (i.e. "-a1" is always an error), hence the + // result.Count == 0 check above. + string value = string.Empty; + + while (!reader.ReachedEnd) + { + char c = reader.Peek(); + + if (char.IsWhiteSpace(c)) + { + break; + } + + value += c.ToString(CultureInfo.InvariantCulture); + reader.Read(); + } + + value = "-" + value; // Prefix with the minus sign that we originally thought to mean a short option + result.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value)); + } + else + { // Create a token representing the short option. - var tokenPosition = position + 1 + result.Count; - var represntation = current.ToString(CultureInfo.InvariantCulture); - var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, represntation, represntation); + var representation = current.ToString(CultureInfo.InvariantCulture); + var tokenPosition = position + 1 + result.Count; + var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation); + throw CommandParseException.InvalidShortOptionName(reader.Original, token); } } @@ -271,7 +248,7 @@ internal static class CommandTreeTokenizer var name = ScanString(context, reader, new[] { '=', ':' }); // Perform validation of the name. - if (name.Value.Length == 0) + if (name.Value == " ") { throw CommandParseException.LongOptionNameIsMissing(reader, position); } diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt index 7def92f..609a7a6 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt @@ -1,4 +1,4 @@ Error: Flags cannot be assigned a value. - dog --alive foo + dog --alive=indeterminate foo ^^^^^^^ Can't assign value \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt index b3381d2..8027f61 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt @@ -1,4 +1,4 @@ Error: Flags cannot be assigned a value. - dog -a foo + dog -a=indeterminate foo ^^ Can't assign value \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt deleted file mode 100644 index 1c05483..0000000 --- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Raw -/c -set && pause \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt deleted file mode 100644 index a6d7032..0000000 --- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -Error: Encountered unterminated quoted string 'Rufus'. - - --name "Rufus - ^^^^^^ Did you forget the closing quotation mark? \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs index 3ee8305..c195ffd 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs @@ -15,17 +15,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_When_Command_Is_Unknown() { // Given - var fixture = new Fixture(); - fixture.Configure(config => + var app = new CommandAppTester(); + app.Configure(config => { config.AddCommand("dog"); }); // When - var result = fixture.Run("cat", "14"); + var result = app.Run("cat", "14"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -33,17 +33,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("empty"); }); // When - var result = fixture.Run("empty", "other"); + var result = app.Run("empty", "other"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -51,8 +51,8 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.Configure(config => + var app = new CommandAppTester(); + app.Configure(config => { config.AddBranch("dog", a => { @@ -61,10 +61,10 @@ public sealed partial class CommandAppTests }); // When - var result = fixture.Run("dog", "bat", "14"); + var result = app.Run("dog", "bat", "14"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -72,17 +72,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.Configure(config => + var app = new CommandAppTester(); + app.Configure(config => { config.AddCommand("cat"); }); // When - var result = fixture.Run("bat", "14"); + var result = app.Run("bat", "14"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -90,18 +90,18 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.WithDefaultCommand>(); - fixture.Configure(config => + var app = new CommandAppTester(); + app.SetDefaultCommand>(); + app.Configure(config => { config.AddCommand>("cat"); }); // When - var result = fixture.Run("bat"); + var result = app.Run("bat"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -109,9 +109,9 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.WithDefaultCommand>(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.SetDefaultCommand>(); + app.Configure(configurator => { configurator.AddBranch("dog", a => { @@ -120,10 +120,10 @@ public sealed partial class CommandAppTests }); // When - var result = fixture.Run("dog", "bat"); + var result = app.Run("dog", "bat"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -131,18 +131,18 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.WithDefaultCommand>(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.SetDefaultCommand>(); + app.Configure(configurator => { configurator.AddCommand>("bar"); }); // When - var result = fixture.Run("qux", "bat"); + var result = app.Run("qux", "bat"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -150,8 +150,8 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddBranch("foo", a => { @@ -160,10 +160,10 @@ public sealed partial class CommandAppTests }); // When - var result = fixture.Run("foo", "qux", "bat"); + var result = app.Run("foo", "qux", "bat"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -176,17 +176,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Long_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "--alive", "foo"); + var result = app.Run("dog", "--alive=indeterminate", "foo"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -194,17 +194,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Short_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "-a", "foo"); + var result = app.Run("dog", "-a=indeterminate", "foo"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -217,17 +217,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Long_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "--name"); + var result = app.Run("dog", "--name"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -235,17 +235,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Short_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "-n"); + var result = app.Run("dog", "-n"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -258,17 +258,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("giraffe"); }); // When - var result = fixture.Run("giraffe", "foo", "bar", "baz"); + var result = app.Run("giraffe", "foo", "bar", "baz"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -281,17 +281,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Long_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("--foo"); + var result = app.Run("--foo"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -299,17 +299,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Short_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("-f"); + var result = app.Run("-f"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -322,18 +322,18 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.UseStrictParsing(); configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "--unknown"); + var result = app.Run("dog", "--unknown"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -341,42 +341,19 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.UseStrictParsing(); configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "-u"); + var result = app.Run("dog", "-u"); // Then - return Verifier.Verify(result); - } - } - - [UsesVerify] - [ExpectationPath("UnterminatedQuote")] - public sealed class UnterminatedQuote - { - [Fact] - [Expectation("Test_1")] - public Task Should_Return_Correct_Text() - { - // Given - var fixture = new Fixture(); - fixture.Configure(configurator => - { - configurator.AddCommand("dog"); - }); - - // When - var result = fixture.Run("--name", "\"Rufus"); - - // Then - return Verifier.Verify(result); - } + return Verifier.Verify(result.Output); + } } [UsesVerify] @@ -388,17 +365,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Short_Option() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", "-", " "); + var result = app.Run("dog", "-", " "); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -406,17 +383,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"--foo="); + var result = app.Run("dog", $"--foo="); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -424,17 +401,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"--foo:"); + var result = app.Run("dog", $"--foo:"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -442,17 +419,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"-f="); + var result = app.Run("dog", $"-f="); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Fact] @@ -460,17 +437,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"-f:"); + var result = app.Run("dog", $"-f:"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -483,17 +460,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"-f0o"); + var result = app.Run("dog", $"-f0o"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -506,17 +483,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"--f"); + var result = app.Run("dog", $"--f"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -529,17 +506,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"-- "); + var result = app.Run("dog", $"-- "); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -552,17 +529,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"--1foo"); + var result = app.Run("dog", $"--1foo"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } } @@ -575,17 +552,17 @@ public sealed partial class CommandAppTests public Task Should_Return_Correct_Text() { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", $"--f€oo"); + var result = app.Run("dog", $"--f€oo"); // Then - return Verifier.Verify(result); + return Verifier.Verify(result.Output); } [Theory] @@ -596,71 +573,18 @@ public sealed partial class CommandAppTests public void Should_Allow_Special_Symbols_In_Name(string option) { // Given - var fixture = new Fixture(); - fixture.Configure(configurator => + var app = new CommandAppTester(); + app.Configure(configurator => { configurator.AddCommand("dog"); }); // When - var result = fixture.Run("dog", option); + var result = app.Run("dog", option); // Then - result.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'."); - } - } - - [Fact] - [Expectation("Quoted_Strings")] - public Task Should_Parse_Quoted_Strings_Correctly() - { - // Given - var fixture = new Fixture(); - fixture.Configure(configurator => - { - configurator.AddCommand("foo"); - }); - - // When - var result = fixture.Run("foo", "--", "/c", "\"set && pause\""); - - // Then - return Verifier.Verify(result); - } - } - - internal sealed class Fixture - { - private Action _appConfiguration = _ => { }; - private Action _configuration; - - public void WithDefaultCommand() - where T : class, ICommand - { - _appConfiguration = (app) => app.SetDefaultCommand(); - } - - public void Configure(Action action) - { - _configuration = action; - } - - public string Run(params string[] args) - { - using (var console = new TestConsole()) - { - var app = new CommandApp(); - _appConfiguration?.Invoke(app); - - app.Configure(_configuration); - app.Configure(c => c.ConfigureConsole(console)); - app.Run(args); - - return console.Output - .NormalizeLineEndings() - .TrimLines() - .Trim(); - } + result.Output.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'."); + } } } } diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs index ba1b31a..5425123 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs @@ -62,9 +62,38 @@ public sealed partial class CommandAppTests result.Context.Remaining.Raw[0].ShouldBe("--foo"); result.Context.Remaining.Raw[1].ShouldBe("bar"); result.Context.Remaining.Raw[2].ShouldBe("-bar"); - result.Context.Remaining.Raw[3].ShouldBe("baz"); + result.Context.Remaining.Raw[3].ShouldBe("\"baz\""); result.Context.Remaining.Raw[4].ShouldBe("qux"); result.Context.Remaining.Raw[5].ShouldBe("foo bar baz qux"); } + + [Fact] + public void Should_Preserve_Quotes_Hyphen_Delimiters() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", "dog", "12", "--", + "/c", "\"set && pause\"", + "Name=\" -Rufus --' ", + }); + + // Then + result.Context.Remaining.Raw.Count.ShouldBe(3); + result.Context.Remaining.Raw[0].ShouldBe("/c"); + result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\""); + result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' "); + } } } diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs index 8175ace..9021b3c 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs @@ -132,10 +132,43 @@ public sealed partial class CommandAppTests dog.IsAlive.ShouldBe(false); dog.Name.ShouldBe("Rufus"); }); - } - + } + [Fact] public void Should_Pass_Case_5() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "--alive", "4", "dog", "--good-boy", "12", + "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.IsAlive.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Pass_Case_6() { // Given var app = new CommandAppTester(); @@ -164,7 +197,7 @@ public sealed partial class CommandAppTests } [Fact] - public void Should_Pass_Case_6() + public void Should_Pass_Case_7() { // Given var app = new CommandAppTester(); @@ -189,6 +222,38 @@ public sealed partial class CommandAppTests }); } + [Fact] + public void Should_Preserve_Quotes_Hyphen_Delimiters_Spaces() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + "--name=\" -Rufus --' ", + "--", + "--order-by", "\"-size\"", + "--order-by", " ", + "--order-by", string.Empty, + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Name.ShouldBe("\" -Rufus --' "); + }); + result.Context.Remaining.Parsed.Count.ShouldBe(1); + result.Context.ShouldHaveRemainingArgument("order-by", values: new[] { "\"-size\"", " ", string.Empty }); + } + [Fact] public void Should_Be_Able_To_Use_Command_Alias() { @@ -489,6 +554,181 @@ public sealed partial class CommandAppTests { dog.IsAlive.ShouldBe(expected); }); + } + + [Fact] + public void Should_Set_Short_Option_Before_Argument() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] { "dog", "-a", "-n=Rufus", "4", "12", }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(settings => + { + settings.IsAlive.ShouldBeTrue(); + settings.Name.ShouldBe("Rufus"); + settings.Legs.ShouldBe(4); + settings.Age.ShouldBe(12); + }); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("false", false)] + [InlineData("False", false)] + public void Should_Set_Short_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] { "dog", "-a", value, "4", "12", }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(settings => + { + settings.IsAlive.ShouldBe(expected); + settings.Legs.ShouldBe(4); + settings.Age.ShouldBe(12); + }); + } + + [Fact] + public void Should_Set_Long_Option_Before_Argument() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] { "dog", "--alive", "--name=Rufus", "4", "12" }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(settings => + { + settings.IsAlive.ShouldBeTrue(); + settings.Name.ShouldBe("Rufus"); + settings.Legs.ShouldBe(4); + settings.Age.ShouldBe(12); + }); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("false", false)] + [InlineData("False", false)] + public void Should_Set_Long_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] { "dog", "--alive", value, "4", "12", }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(settings => + { + settings.IsAlive.ShouldBe(expected); + settings.Legs.ShouldBe(4); + settings.Age.ShouldBe(12); + }); + } + + [Theory] + + // Long options + [InlineData("dog --alive 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog --alive=true 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog --alive:true 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog --alive --good-boy 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog --alive=true --good-boy=true 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog --alive:true --good-boy:true 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog --alive --good-boy --name Rufus 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog --alive=true --good-boy=true --name Rufus 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog --alive:true --good-boy:true --name Rufus 4 12", 4, 12, true, true, "Rufus")] + + // Short options + [InlineData("dog -a 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog -a=true 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog -a:true 4 12 --name Rufus", 4, 12, false, true, "Rufus")] + [InlineData("dog -a --good-boy 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog -a=true -g=true 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog -a:true -g:true 4 12 --name Rufus", 4, 12, true, true, "Rufus")] + [InlineData("dog -a -g --name Rufus 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog -a=true -g=true --name Rufus 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog -a:true -g:true --name Rufus 4 12", 4, 12, true, true, "Rufus")] + + // Switch around ordering of the options + [InlineData("dog --good-boy:true --name Rufus --alive:true 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog --name Rufus --alive:true --good-boy:true 4 12", 4, 12, true, true, "Rufus")] + [InlineData("dog --name Rufus --good-boy:true --alive:true 4 12", 4, 12, true, true, "Rufus")] + + // Inject the command arguments in between the options + [InlineData("dog 4 12 --good-boy:true --name Rufus --alive:true", 4, 12, true, true, "Rufus")] + [InlineData("dog 4 --good-boy:true 12 --name Rufus --alive:true", 4, 12, true, true, "Rufus")] + [InlineData("dog --good-boy:true 4 12 --name Rufus --alive:true", 4, 12, true, true, "Rufus")] + [InlineData("dog --good-boy:true 4 --name Rufus 12 --alive:true", 4, 12, true, true, "Rufus")] + [InlineData("dog --name Rufus --alive:true 4 12 --good-boy:true", 4, 12, true, true, "Rufus")] + [InlineData("dog --name Rufus --alive:true 4 --good-boy:true 12", 4, 12, true, true, "Rufus")] + + // Inject the command arguments in between the options (all flag values set to false) + [InlineData("dog 4 12 --good-boy:false --name Rufus --alive:false", 4, 12, false, false, "Rufus")] + [InlineData("dog 4 --good-boy:false 12 --name Rufus --alive:false", 4, 12, false, false, "Rufus")] + [InlineData("dog --good-boy:false 4 12 --name Rufus --alive:false", 4, 12, false, false, "Rufus")] + [InlineData("dog --good-boy:false 4 --name Rufus 12 --alive:false", 4, 12, false, false, "Rufus")] + [InlineData("dog --name Rufus --alive:false 4 12 --good-boy:false", 4, 12, false, false, "Rufus")] + [InlineData("dog --name Rufus --alive:false 4 --good-boy:false 12", 4, 12, false, false, "Rufus")] + public void Should_Set_Option_Before_Argument(string arguments, int legs, int age, bool goodBoy, bool isAlive, string name) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(arguments.Split(' ')); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(settings => + { + settings.Legs.ShouldBe(legs); + settings.Age.ShouldBe(age); + settings.GoodBoy.ShouldBe(goodBoy); + settings.IsAlive.ShouldBe(isAlive); + settings.Name.ShouldBe(name); + }); } [Fact] @@ -805,7 +1045,7 @@ public sealed partial class CommandAppTests result.Context.Remaining.Raw[0].ShouldBe("--foo"); result.Context.Remaining.Raw[1].ShouldBe("bar"); result.Context.Remaining.Raw[2].ShouldBe("-bar"); - result.Context.Remaining.Raw[3].ShouldBe("baz"); + result.Context.Remaining.Raw[3].ShouldBe("\"baz\""); result.Context.Remaining.Raw[4].ShouldBe("qux"); } } diff --git a/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs b/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs new file mode 100644 index 0000000..d968224 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs @@ -0,0 +1,314 @@ +namespace Spectre.Console.Tests.Unit.Cli.Parsing; + +public class CommandTreeTokenizerTests +{ + public sealed class ScanString + { + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + [InlineData("\t")] + [InlineData("\r\n\t")] + [InlineData("👋🏻")] + [InlineData("🐎👋🏻🔥❤️")] + [InlineData("\"🐎👋🏻🔥❤️\" is an emoji sequence")] + public void Should_Preserve_Edgecase_Inputs(string actualAndExpected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(actualAndExpected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String); + } + + [Theory] + + // Double-quote handling + [InlineData("\"")] + [InlineData("\"\"")] + [InlineData("\"Rufus\"")] + [InlineData("\" Rufus\"")] + [InlineData("\"-R\"")] + [InlineData("\"-Rufus\"")] + [InlineData("\" -Rufus\"")] + + // Single-quote handling + [InlineData("'")] + [InlineData("''")] + [InlineData("'Rufus'")] + [InlineData("' Rufus'")] + [InlineData("'-R'")] + [InlineData("'-Rufus'")] + [InlineData("' -Rufus'")] + public void Should_Preserve_Quotes(string actualAndExpected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(actualAndExpected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String); + } + + [Theory] + [InlineData("Rufus-")] + [InlineData("Rufus--")] + [InlineData("R-u-f-u-s")] + public void Should_Preserve_Hyphen_Delimiters(string actualAndExpected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(actualAndExpected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String); + } + + [Theory] + [InlineData(" Rufus")] + [InlineData("Rufus ")] + [InlineData(" Rufus ")] + public void Should_Preserve_Spaces(string actualAndExpected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(actualAndExpected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String); + } + + [Theory] + [InlineData(" \" -Rufus -- ")] + [InlineData("Name=\" -Rufus --' ")] + public void Should_Preserve_Quotes_Hyphen_Delimiters_Spaces(string actualAndExpected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(actualAndExpected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String); + } + } + + public sealed class ScanLongOption + { + [Theory] + [InlineData("--Name-", "Name-")] + [InlineData("--Name_", "Name_")] + public void Should_Allow_Hyphens_And_Underscores_In_Option_Name(string actual, string expected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actual }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(expected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.LongOption); + } + + [Theory] + [InlineData("-- ")] + [InlineData("--Name ")] + [InlineData("--Name\"")] + [InlineData("--Nam\"e")] + public void Should_Throw_On_Invalid_Option_Name(string actual) + { + // Given + + // When + var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Invalid long option name."); + }); + } + } + + public sealed class ScanShortOptions + { + [Fact] + public void Should_Accept_Option_Without_Value() + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new[] { "-a" }); + + // Then + result.Remaining.ShouldBeEmpty(); + result.Tokens.ShouldHaveSingleItem(); + + var t = result.Tokens[0]; + t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(0); + t.Value.ShouldBe("a"); + t.Representation.ShouldBe("-a"); + } + + [Theory] + [InlineData("-a:foo")] + [InlineData("-a=foo")] + public void Should_Accept_Option_With_Value(string param) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new[] { param }); + + // Then + result.Remaining.ShouldBeEmpty(); + result.Tokens.Count.ShouldBe(2); + + var t = result.Tokens.Consume(); + t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(0); + t.Value.ShouldBe("a"); + t.Representation.ShouldBe("-a"); + + t = result.Tokens.Consume(); + t.TokenKind.ShouldBe(CommandTreeToken.Kind.String); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(3); + t.Value.ShouldBe("foo"); + t.Representation.ShouldBe("foo"); + } + + [Theory] + + // Positive values + [InlineData("-a:1.5", null, "1.5")] + [InlineData("-a=1.5", null, "1.5")] + [InlineData("-a", "1.5", "1.5")] + + // Negative values + [InlineData("-a:-1.5", null, "-1.5")] + [InlineData("-a=-1.5", null, "-1.5")] + [InlineData("-a", "-1.5", "-1.5")] + public void Should_Accept_Option_With_Numeric_Value(string firstArg, string secondArg, string expectedValue) + { + // Given + List args = new List(); + args.Add(firstArg); + if (secondArg != null) + { + args.Add(secondArg); + } + + // When + var result = CommandTreeTokenizer.Tokenize(args); + + // Then + result.Remaining.ShouldBeEmpty(); + result.Tokens.Count.ShouldBe(2); + + var t = result.Tokens.Consume(); + t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(0); + t.Value.ShouldBe("a"); + t.Representation.ShouldBe("-a"); + + t = result.Tokens.Consume(); + t.TokenKind.ShouldBe(CommandTreeToken.Kind.String); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(3); + t.Value.ShouldBe(expectedValue); + t.Representation.ShouldBe(expectedValue); + } + + [Fact] + public void Should_Accept_Option_With_Negative_Numeric_Prefixed_String_Value() + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new[] { "-6..2 " }); + + // Then + result.Remaining.ShouldBeEmpty(); + result.Tokens.ShouldHaveSingleItem(); + + var t = result.Tokens[0]; + t.TokenKind.ShouldBe(CommandTreeToken.Kind.String); + t.IsGrouped.ShouldBe(false); + t.Position.ShouldBe(0); + t.Value.ShouldBe("-6..2"); + t.Representation.ShouldBe("-6..2"); + } + + [Theory] + [InlineData("-N ", "N")] + public void Should_Remove_Trailing_Spaces_In_Option_Name(string actual, string expected) + { + // Given + + // When + var result = CommandTreeTokenizer.Tokenize(new string[] { actual }); + + // Then + result.Tokens.Count.ShouldBe(1); + result.Tokens[0].Value.ShouldBe(expected); + result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption); + } + + [Theory] + [InlineData("-N-")] + [InlineData("-N\"")] + [InlineData("-a1")] + public void Should_Throw_On_Invalid_Option_Name(string actual) + { + // Given + + // When + var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Short option does not have a valid name."); + }); + } + } + + [Theory] + [InlineData("-")] + [InlineData("- ")] + public void Should_Throw_On_Missing_Option_Name(string actual) + { + // Given + + // When + var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Option does not have a name."); + }); + } +} diff --git a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt deleted file mode 100644 index 1c05483..0000000 --- a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Raw -/c -set && pause \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt deleted file mode 100644 index a6d7032..0000000 --- a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -Error: Encountered unterminated quoted string 'Rufus'. - - --name "Rufus - ^^^^^^ Did you forget the closing quotation mark? \ No newline at end of file