Command line improvements (#1103)

Closes #187
Closes #203
Closes #1059
This commit is contained in:
Frank Ray
2023-04-02 21:43:21 +01:00
committed by GitHub
parent 70da3f40ff
commit 714cf179cb
24 changed files with 1052 additions and 269 deletions

View File

@ -0,0 +1,351 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed class Branches
{
[Fact]
public void Should_Run_The_Default_Command_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Throw_When_No_Default_Command_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal => { });
});
// When
var result = Record.Exception(() =>
{
app.Run(new[]
{
"animal", "4",
});
});
// Then
result.ShouldBeOfType<CommandConfigurationException>().And(ex =>
{
ex.Message.ShouldBe("The branch 'animal' does not define any commands.");
});
}
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:SingleLineCommentMustBePrecededByBlankLine", Justification = "Helps to illustrate the expected behaviour of this unit test.")]
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1005:SingleLineCommentsMustBeginWithSingleSpace", Justification = "Helps to illustrate the expected behaviour of this unit test.")]
[Fact]
public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Relaxed_Parsing()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = app.Run(new[]
{
// The CommandTreeParser should be unable to determine which command line
// arguments belong to the branch and which belong to the branch's
// default command (once inserted).
"animal", "4", "--name", "Kitty",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>().And(cat =>
{
cat.Legs.ShouldBe(4);
//cat.Name.ShouldBe("Kitty"); //<-- Should normally be correct, but instead name will be added to the remaining arguments (see below).
});
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("name", values: new[] { "Kitty", });
}
[Fact]
public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Strict_Parsing()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.UseStrictParsing();
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = Record.Exception(() =>
{
app.Run(new[]
{
// The CommandTreeParser should be unable to determine which command line
// arguments belong to the branch and which belong to the branch's
// default command (once inserted).
"animal", "4", "--name", "Kitty",
});
});
// Then
result.ShouldBeOfType<CommandParseException>().And(ex =>
{
ex.Message.ShouldBe("Unknown option 'name'.");
});
}
[Fact]
public void Should_Run_The_Default_Command_On_Branch_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<CatCommand>();
});
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "mammal",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Run_The_Default_Command_On_Branch_On_Branch_With_Arguments()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<CatCommand>();
});
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "mammal", "--name", "Kitty",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>().And(cat =>
{
cat.Legs.ShouldBe(4);
cat.Name.ShouldBe("Kitty");
});
}
[Fact]
public void Should_Run_The_Default_Command_Not_The_Named_Command_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Run_The_Named_Command_Not_The_Default_Command_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<LionCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "dog", "12", "--good-boy", "--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.Name.ShouldBe("Rufus");
});
}
[Fact]
public void Should_Allow_Multiple_Branches_Multiple_Commands()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.AddCommand<DogCommand>("dog");
mammal.AddCommand<CatCommand>("cat");
});
});
});
// When
var result = app.Run(new[]
{
"animal", "--alive", "mammal", "--name",
"Rufus", "dog", "12", "--good-boy",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{
dog.Age.ShouldBe(12);
dog.GoodBoy.ShouldBe(true);
dog.Name.ShouldBe("Rufus");
dog.IsAlive.ShouldBe(true);
});
}
[Fact]
public void Should_Allow_Single_Branch_Multiple_Commands()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.AddCommand<CatCommand>("cat");
});
});
// When
var result = app.Run(new[]
{
"animal", "dog", "12", "--good-boy",
"--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{
dog.Age.ShouldBe(12);
dog.GoodBoy.ShouldBe(true);
dog.Name.ShouldBe("Rufus");
dog.IsAlive.ShouldBe(false);
});
}
[Fact]
public void Should_Allow_Single_Branch_Single_Command()
{
// 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", "--good-boy",
"--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(false);
dog.Name.ShouldBe("Rufus");
});
}
}
}

View File

@ -3,7 +3,167 @@ namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed class Remaining
{
{
[Theory]
[InlineData("-a")]
[InlineData("--alive")]
public void Should_Not_Add_Known_Flags_To_Remaining_Arguments_RelaxedParsing(string knownFlag)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
knownFlag,
});
// Then
result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{
dog.IsAlive.ShouldBe(true);
});
result.Context.Remaining.Parsed.Count.ShouldBe(0);
result.Context.Remaining.Raw.Count.ShouldBe(0);
}
[Theory]
[InlineData("-r")]
[InlineData("--romeo")]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_RelaxedParsing(string unknownFlag)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
unknownFlag,
});
// Then
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument(unknownFlag.TrimStart('-'), values: new[] { (string)null });
result.Context.Remaining.Raw.Count.ShouldBe(0);
}
[Fact]
public void Should_Add_Unknown_Flags_When_Grouped_To_Remaining_Arguments_RelaxedParsing()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
"-agr",
});
// Then
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("r", values: new[] { (string)null });
result.Context.Remaining.Raw.Count.ShouldBe(0);
}
[Theory]
[InlineData("-a")]
[InlineData("--alive")]
public void Should_Not_Add_Known_Flags_To_Remaining_Arguments_StrictParsing(string knownFlag)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.UseStrictParsing();
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
knownFlag,
});
// Then
result.Context.Remaining.Parsed.Count.ShouldBe(0);
result.Context.Remaining.Raw.Count.ShouldBe(0);
}
[Theory]
[InlineData("-r")]
[InlineData("--romeo")]
public void Should_Not_Add_Unknown_Flags_To_Remaining_Arguments_StrictParsing(string unknownFlag)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.UseStrictParsing();
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = Record.Exception(() => app.Run(new[]
{
"dog", "12", "4",
unknownFlag,
}));
// Then
result.ShouldBeOfType<CommandParseException>().And(ex =>
{
ex.Message.ShouldBe($"Unknown option '{unknownFlag.TrimStart('-')}'.");
});
}
[Fact]
public void Should_Not_Add_Unknown_Flags_When_Grouped_To_Remaining_Arguments_StrictParsing()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.UseStrictParsing();
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = Record.Exception(() => app.Run(new[]
{
"dog", "12", "4",
"-agr",
}));
// Then
result.ShouldBeOfType<CommandParseException>().And(ex =>
{
ex.Message.ShouldBe($"Unknown option 'r'.");
});
}
[Fact]
public void Should_Register_Remaining_Parsed_Arguments_With_Context()
{
@ -94,6 +254,35 @@ public sealed partial class CommandAppTests
result.Context.Remaining.Raw[0].ShouldBe("/c");
result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\"");
result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' ");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Convert_Flags_To_Remaining_Arguments_If_Cannot_Be_Assigned(bool useStrictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.ConvertFlagsToRemainingArguments = true;
config.Settings.StrictParsing = useStrictParsing;
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
"--good-boy=Please be good Rufus!",
});
// Then
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("good-boy", values: new[] { "Please be good Rufus!" });
result.Context.Remaining.Raw.Count.ShouldBe(0); // nb. there are no "raw" remaining arguments on the command line
}
}
}

View File

@ -130,6 +130,77 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Test_7")]
public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Branch_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<HorseCommand>();
});
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Test_8")]
public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Command_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Test_9")]
public Task Should_Dump_Correct_Model_For_Model_With_Default_Command_Single_Branch_Single_Command_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<EmptyCommand>();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Hidden_Command_Options")]
public Task Should_Not_Dump_Hidden_Options_On_A_Command()

View File

@ -4,42 +4,6 @@ public sealed partial class CommandAppTests
{
[Fact]
public void Should_Pass_Case_1()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.AddCommand<DogCommand>("dog");
mammal.AddCommand<HorseCommand>("horse");
});
});
});
// When
var result = app.Run(new[]
{
"animal", "--alive", "mammal", "--name",
"Rufus", "dog", "12", "--good-boy",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{
dog.Age.ShouldBe(12);
dog.GoodBoy.ShouldBe(true);
dog.Name.ShouldBe("Rufus");
dog.IsAlive.ShouldBe(true);
});
}
[Fact]
public void Should_Pass_Case_2()
{
// Given
var app = new CommandAppTester();
@ -52,8 +16,8 @@ public sealed partial class CommandAppTests
// When
var result = app.Run(new[]
{
"dog", "12", "4", "--good-boy",
"--name", "Rufus", "--alive",
"dog", "12", "4", "--good-boy",
"--name", "Rufus", "--alive",
});
// Then
@ -69,7 +33,7 @@ public sealed partial class CommandAppTests
}
[Fact]
public void Should_Pass_Case_3()
public void Should_Pass_Case_2()
{
// Given
var app = new CommandAppTester();
@ -197,7 +161,7 @@ public sealed partial class CommandAppTests
}
[Fact]
public void Should_Pass_Case_7()
public void Should_Pass_Case_3()
{
// Given
var app = new CommandAppTester();
@ -904,6 +868,86 @@ public sealed partial class CommandAppTests
result.Context.ShouldHaveRemainingArgument("foo", values: new[] { (string)null });
}
[Fact]
public void Should_Run_The_Default_Command()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"4", "12", "--good-boy", "--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.Name.ShouldBe("Rufus");
});
}
[Fact]
public void Should_Run_The_Default_Command_Not_The_Named_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<HorseCommand>("horse");
});
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"4", "12", "--good-boy", "--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.Name.ShouldBe("Rufus");
});
}
[Fact]
public void Should_Run_The_Named_Command_Not_The_Default_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<HorseCommand>("horse");
});
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"horse", "4", "--name", "Arkle",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<HorseSettings>().And(horse =>
{
horse.Legs.ShouldBe(4);
horse.Name.ShouldBe("Arkle");
});
}
[Fact]
public void Should_Set_Command_Name_In_Context()
{
@ -1081,67 +1125,4 @@ public sealed partial class CommandAppTests
data.ShouldBe(2);
}
}
public sealed class Remaining_Arguments
{
[Fact]
public void Should_Register_Remaining_Parsed_Arguments_With_Context()
{
// 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", "--",
"--foo", "bar", "--foo", "baz",
"-bar", "\"baz\"", "qux",
});
// Then
result.Context.Remaining.Parsed.Count.ShouldBe(4);
result.Context.ShouldHaveRemainingArgument("foo", values: new[] { "bar", "baz" });
result.Context.ShouldHaveRemainingArgument("b", values: new[] { (string)null });
result.Context.ShouldHaveRemainingArgument("a", values: new[] { (string)null });
result.Context.ShouldHaveRemainingArgument("r", values: new[] { (string)null });
}
[Fact]
public void Should_Register_Remaining_Raw_Arguments_With_Context()
{
// 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", "--",
"--foo", "bar", "-bar", "\"baz\"", "qux",
});
// Then
result.Context.Remaining.Raw.Count.ShouldBe(5);
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[4].ShouldBe("qux");
}
}
}