-
Notifications
You must be signed in to change notification settings - Fork 640
Fixes #587 Support negative numbers as command option values #732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -235,13 +235,34 @@ private static IEnumerable<CommandTreeToken> ScanShortOptions(CommandTreeTokeniz | |
| ? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}") | ||
| : new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value)); | ||
| } | ||
| 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); | ||
| throw CommandParseException.InvalidShortOptionName(reader.Original, token); | ||
| ThrowInvalidShortOptionName(current.ToString(CultureInfo.InvariantCulture)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not see the benefit of putting the local three-line logic into a separate private method, which also still references |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -254,6 +275,13 @@ private static IEnumerable<CommandTreeToken> ScanShortOptions(CommandTreeTokeniz | |
| } | ||
|
|
||
| return result; | ||
|
|
||
| void ThrowInvalidShortOptionName(string representation) | ||
| { | ||
| var tokenPosition = position + 1 + result.Count; | ||
| var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation); | ||
| throw CommandParseException.InvalidShortOptionName(reader.Original, token); | ||
| } | ||
| } | ||
|
|
||
| private static CommandTreeToken ScanLongOption(CommandTreeTokenizerContext context, TextBuffer reader, int position) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| namespace Spectre.Console.Tests.Unit.Cli; | ||
|
|
||
| public static class CommandTreeTokenizerTests | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fantastic that you have introduced explicit unit tests for the tokenizer! A positive move away from the general approach of 'unit tests' being located at the CommandApp level (nb. more like integration tests really). Does this class need to be static? Unit test classes aren't generally made static - suggest this is also removed to ensure consistency with the rest of the codebase. |
||
| { | ||
| public sealed class ShortOptions | ||
| { | ||
| [Fact] | ||
| public void Valueless() | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // When |
||
| var result = CommandTreeTokenizer.Tokenize(new[] { "-a" }); | ||
| Assert.Empty(result.Remaining); | ||
| var t = Assert.Single(result.Tokens); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // Then |
||
| 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 With_Value(string param) | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // When |
||
| var result = CommandTreeTokenizer.Tokenize(new[] { param }); | ||
| Assert.Empty(result.Remaining); | ||
| Assert.Equal(2, result.Tokens.Count); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // Then |
||
| 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] | ||
| [InlineData("-a:-1.5", null)] | ||
| [InlineData("-a=-1.5", null)] | ||
| [InlineData("-a", "-1.5")] | ||
| public void With_Negative_Numeric_Value(string firstArg, string secondArg) | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // Given |
||
| List<string> args = new List<string>(); | ||
| args.Add(firstArg); | ||
| if (secondArg != null) | ||
| { | ||
| args.Add(secondArg); | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // When |
||
| var result = CommandTreeTokenizer.Tokenize(args); | ||
| Assert.Empty(result.Remaining); | ||
| Assert.Equal(2, result.Tokens.Count); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // Then |
||
| 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("-1.5"); | ||
| t.Representation.ShouldBe("-1.5"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void With_Negative_Numeric_Prefixed_String_Value() | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // When |
||
| var result = CommandTreeTokenizer.Tokenize(new[] { "-6..2 " }); | ||
| Assert.Empty(result.Remaining); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // Then |
||
| var t = Assert.Single(result.Tokens); | ||
| t.TokenKind.ShouldBe(CommandTreeToken.Kind.String); | ||
| t.IsGrouped.ShouldBe(false); | ||
| t.Position.ShouldBe(0); | ||
| t.Value.ShouldBe("-6..2"); | ||
| t.Representation.ShouldBe("-6..2"); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everywhere else in the codebase, multi-line comments have simply used the // convention at the beginning of each line. I suggest this comment be updated accordingly to ensure consistency (for better or worse) with the rest of the codebase.