mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-14 16:02:50 +08:00
Command line argument parsing improvements (#1048)
* Support negative numbers as command option values * Support command line options before arguments * POSIX-compliant handling of quotes (double and single, terminated and unterminated), whitespace, hyphens, and special characters (e.g. emojis)
This commit is contained in:
parent
f895bb175d
commit
b793482ebb
@ -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<string> args, CommandTreeToken token)
|
||||
{
|
||||
var suggestion = CommandSuggestor.Suggest(model, node?.Command, token.Value);
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a separater was encountered immediately before the <see cref="CommandTreeToken.Value"/>.
|
||||
/// </summary>
|
||||
public bool HadSeparator { get; set; }
|
||||
|
||||
public enum Kind
|
||||
{
|
||||
|
@ -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<CommandTreeToken> 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<CommandTreeToken> 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);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
Error: Flags cannot be assigned a value.
|
||||
|
||||
dog --alive foo
|
||||
dog --alive=indeterminate foo
|
||||
^^^^^^^ Can't assign value
|
@ -1,4 +1,4 @@
|
||||
Error: Flags cannot be assigned a value.
|
||||
|
||||
dog -a foo
|
||||
dog -a=indeterminate foo
|
||||
^^ Can't assign value
|
@ -1,3 +0,0 @@
|
||||
# Raw
|
||||
/c
|
||||
set && pause
|
@ -1,4 +0,0 @@
|
||||
Error: Encountered unterminated quoted string 'Rufus'.
|
||||
|
||||
--name "Rufus
|
||||
^^^^^^ Did you forget the closing quotation mark?
|
@ -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<DogCommand>("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<EmptyCommand>("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<CommandSettings>("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<CatCommand>("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<GenericCommand<EmptyCommandSettings>>();
|
||||
fixture.Configure(config =>
|
||||
var app = new CommandAppTester();
|
||||
app.SetDefaultCommand<GenericCommand<EmptyCommandSettings>>();
|
||||
app.Configure(config =>
|
||||
{
|
||||
config.AddCommand<GenericCommand<EmptyCommandSettings>>("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<GenericCommand<EmptyCommandSettings>>();
|
||||
fixture.Configure(configurator =>
|
||||
var app = new CommandAppTester();
|
||||
app.SetDefaultCommand<GenericCommand<EmptyCommandSettings>>();
|
||||
app.Configure(configurator =>
|
||||
{
|
||||
configurator.AddBranch<CommandSettings>("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<GenericCommand<FooCommandSettings>>();
|
||||
fixture.Configure(configurator =>
|
||||
var app = new CommandAppTester();
|
||||
app.SetDefaultCommand<GenericCommand<FooCommandSettings>>();
|
||||
app.Configure(configurator =>
|
||||
{
|
||||
configurator.AddCommand<GenericCommand<BarCommandSettings>>("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<FooCommandSettings>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<GiraffeCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DumpRemainingCommand>("foo");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = fixture.Run("foo", "--", "/c", "\"set && pause\"");
|
||||
|
||||
// Then
|
||||
return Verifier.Verify(result);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Fixture
|
||||
{
|
||||
private Action<CommandApp> _appConfiguration = _ => { };
|
||||
private Action<IConfigurator> _configuration;
|
||||
|
||||
public void WithDefaultCommand<T>()
|
||||
where T : class, ICommand
|
||||
{
|
||||
_appConfiguration = (app) => app.SetDefaultCommand<T>();
|
||||
}
|
||||
|
||||
public void Configure(Action<IConfigurator> 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'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<AnimalSettings>("animal", animal =>
|
||||
{
|
||||
animal.AddCommand<DogCommand>("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 --' ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<AnimalSettings>("animal", animal =>
|
||||
{
|
||||
animal.AddCommand<DogCommand>("dog");
|
||||
});
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(new[]
|
||||
{
|
||||
"animal", "--alive", "4", "dog", "--good-boy", "12",
|
||||
"--name", "Rufus",
|
||||
});
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("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<DogSettings>().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<DogCommand>("dog");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(new[] { "dog", "-a", "-n=Rufus", "4", "12", });
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("dog");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(new[] { "dog", "-a", value, "4", "12", });
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("dog");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(new[] { "dog", "--alive", "--name=Rufus", "4", "12" });
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("dog");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(new[] { "dog", "--alive", value, "4", "12", });
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("dog");
|
||||
});
|
||||
|
||||
// When
|
||||
var result = app.Run(arguments.Split(' '));
|
||||
|
||||
// Then
|
||||
result.ExitCode.ShouldBe(0);
|
||||
result.Settings.ShouldBeOfType<DogSettings>().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");
|
||||
}
|
||||
}
|
||||
|
@ -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<CommandParseException>().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<string> args = new List<string>();
|
||||
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<CommandParseException>().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<CommandParseException>().And(ex =>
|
||||
{
|
||||
ex.Message.ShouldBe("Option does not have a name.");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# Raw
|
||||
/c
|
||||
set && pause
|
@ -1,4 +0,0 @@
|
||||
Error: Encountered unterminated quoted string 'Rufus'.
|
||||
|
||||
--name "Rufus
|
||||
^^^^^^ Did you forget the closing quotation mark?
|
Loading…
x
Reference in New Issue
Block a user