Compare commits

..

36 Commits

Author SHA1 Message Date
Elisha Aguilera
018f4ebd17 Minor refactors (#1081) 2023-05-14 15:30:27 +01:00
Andrii Rublov
404b052a5f Add ability to pass example args using params syntax (#1166) 2023-05-12 12:08:42 +01:00
Cédric Luthi
dac2097321 Add support for arrays in [DefaultValue] attributes (#1164)
Fixes #1163
2023-05-11 14:26:53 +01:00
Cédric Luthi
6acf9b8c63 Add an implicit operator to convert from Color to Style (#1160) 2023-05-10 14:20:12 +01:00
Patrik Svensson
3ec0fee795 Merge pull request #1218 from phillip-haydon/patch-1 2023-04-24 08:56:25 +02:00
Phillip Haydon
5843a4545e Fix coconut spelling 2023-04-24 14:37:06 +08:00
Patrik Svensson
d64d3ec770 Merge pull request #1211 from MaxAtoms/main 2023-04-03 16:32:18 +02:00
Thomas M. Schöller
4dcbd30285 Merge branch 'spectreconsole:main' into main 2023-04-03 12:45:34 +02:00
MaxAtoms
1334319dd1 Add Alacritty as supported Ansi console 2023-04-03 12:42:24 +02:00
Frank Ray
714cf179cb Command line improvements (#1103)
Closes #187
Closes #203
Closes #1059
2023-04-02 22:43:21 +02:00
Patrik Svensson
70da3f40ff Merge pull request #1161 from MartinZikmund/dev/mazi/stringcomparer-confirmation 2023-03-16 09:57:15 +01:00
Martin Zikmund
5075732564 Adjustments based on code review 2023-03-16 09:45:42 +01:00
Gérald Barré
10467a2912 Do not register analyzers if SpectreConsole is not available in the current compilation 2023-03-13 20:07:41 -04:00
Gérald Barré
a7ab1b690e Use SymbolEqualityComparer.Default when possible 2023-03-13 20:03:12 -04:00
Gérald Barré
142942b8a2 Simplify access to the SemanticModel in analyzers 2023-03-13 19:58:40 -04:00
Gérald Barré
0bab835293 Forward CancellationToken to GetOperation 2023-03-13 19:57:08 -04:00
Patrik Svensson
a6af9a6842 Merge pull request #1183 from Frassle/promptExampleTypo 2023-03-01 18:43:46 +01:00
Cédric Luthi
d3f4f5f208 Add support for converting command parameters into FileInfo and DirectoryInfo (#1145)
Add support for converting command parameters that doesn't have a built-in TypeConverter but has a constructor that takes a string. For CLI apps, FileInfo and DirectoryInfo will likely be the most useful ones, but there may be others.
2023-03-01 12:02:43 +00:00
Fraser Waters
cbbdb3369c Fix minor typo in Prompt example 2023-02-26 22:22:31 +00:00
Gérald Barré
6740f0b02b Add support for static lambda and delegate 2023-02-24 19:04:01 -05:00
Gérald Barré
f7f99ec899 Simplify and make the code fix more robust 2023-02-24 19:04:01 -05:00
Gérald Barré
955fe07bac Allow to apply code fix in top-level statements 2023-02-24 19:04:01 -05:00
Patrik Svensson
819b948e78 Merge pull request #1174 from meziantou/missing-stringcomparison 2023-02-22 16:03:29 +01:00
Gérald Barré
baa8220a52 Use StringComparison.Ordinal instead of culture-sensitive comparisons 2023-02-20 20:23:06 -05:00
Martin Zikmund
9cd7b24e65 Allow configuration of confirmation prompt comparison via StringComparer 2023-02-11 17:09:16 +01:00
Ilya Hryapko
04610cf492 Alias for command branches (#411) 2023-02-09 14:26:06 +00:00
Patrik Svensson
f4183e0462 Merge pull request #1152 from MartinZikmund/dev/mazi/confirmation-insensitive 2023-02-04 20:44:34 +01:00
Martin Zikmund
29846ba505 Ensure correct comparer is used for TextPrompt 2023-02-04 19:56:37 +01:00
Patrik Svensson
92318ce1fb Merge pull request #1151 from MartinZikmund/dev/mazi/confirmation-insensitive 2023-02-04 13:41:14 +01:00
Martin Zikmund
720db951f3 feat: Allow case-insensitive confirmation prompt 2023-02-04 13:24:11 +01:00
Patrik Svensson
e042fe15e4 Merge pull request #1143 from wbaldoumas/alignment-vs-justification-docs-fixes 2023-01-26 03:38:25 +01:00
Will Baldoumas
62b30d6072 Alignment => Justification Docs Fixes 2023-01-25 17:45:30 -08:00
Patrik Svensson
beca0b2419 Merge pull request #1141 from 0xced/better-conversion-errors 2023-01-24 20:15:02 +01:00
Cédric Luthi
de847b90e4 Improve conversion error messages
When a conversion to an enum fails, list all the valid enum values in the error message.

Message before this commit:
> Error: heimday is not a valid value for DayOfWeek.

Message after this commit:
> Error: Failed to convert 'heimday' to DayOfWeek. Valid values are 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
2023-01-24 19:40:11 +01:00
Cédric Luthi
7b9553dd22 Add possibility to set description and/or data for the default command (#1091) 2023-01-12 12:12:27 +00:00
Patrik Svensson
f223f6061c Add blog post for 0.46 release 2023-01-10 04:00:15 +01:00
90 changed files with 2394 additions and 759 deletions

View File

@@ -0,0 +1,81 @@
Title: Spectre.Console 0.46 released!
Description: .NET 7 support, Layout Widget, JSON rendering
Published: 2023-01-10
Category: Release Notes
Excluded: false
---
Happy new year! 🎉
Version 0.46 of Spectre.Console has been released!
A lot has happened since the last release, but the most notable additions
and changes are support for [.NET 7](https://devblogs.microsoft.com/dotnet/announcing-dotnet-7/),
the new [Layout](https://spectreconsole.net/widgets/layout) widget, and
[rendering of JSON](https://spectreconsole.net/widgets/json). There has also been a lot of long overdue work
on the command line argument parser.
## New Contributors
* [@GaryMcD](https://github.com/GaryMcD) made their first contribution in [#961](https://github.com/spectreconsole/spectre.console/pull/961)
* [@eduherminio](https://github.com/eduherminio) made their first contribution in [#964](https://github.com/spectreconsole/spectre.console/pull/964)
* [@Saalvage](https://github.com/Saalvage) made their first contribution in [#976](https://github.com/spectreconsole/spectre.console/pull/976)
* [@BenjaminMichaelis](https://github.com/BenjaminMichaelis) made their first contribution in [#1000](https://github.com/spectreconsole/spectre.console/pull/1000)
* [@nilaoda](https://github.com/nilaoda) made their first contribution in [#1012](https://github.com/spectreconsole/spectre.console/pull/1012)
* [@picture](https://github.com/picture)-vision made their first contribution in [#1013](https://github.com/spectreconsole/spectre.console/pull/1013)
* [@patrickfreilinger](https://github.com/patrickfreilinger) made their first contribution in [#1016](https://github.com/spectreconsole/spectre.console/pull/1016)
* [@sowa](https://github.com/sowa)705 made their first contribution in [#1014](https://github.com/spectreconsole/spectre.console/pull/1014)
* [@ardalis](https://github.com/ardalis) made their first contribution in [#1021](https://github.com/spectreconsole/spectre.console/pull/1021)
* [@Elisha](https://github.com/Elisha)-Aguilera made their first contribution in [#1038](https://github.com/spectreconsole/spectre.console/pull/1038)
* [@wguner](https://github.com/wguner) made their first contribution in [#1044](https://github.com/spectreconsole/spectre.console/pull/1044)
* [@bcwood](https://github.com/bcwood) made their first contribution in [#1068](https://github.com/spectreconsole/spectre.console/pull/1068)
* [@FrankRay](https://github.com/FrankRay)78 made their first contribution in [#1073](https://github.com/spectreconsole/spectre.console/pull/1073)
* [@tomkerkhove](https://github.com/tomkerkhove) made their first contribution in [#1089](https://github.com/spectreconsole/spectre.console/pull/1089)
* [@ArveSystad](https://github.com/ArveSystad) made their first contribution in [#1090](https://github.com/spectreconsole/spectre.console/pull/1090)
* [@maije](https://github.com/maije) made their first contribution in [#1096](https://github.com/spectreconsole/spectre.console/pull/1096)
* [@krisrok](https://github.com/krisrok) made their first contribution in [#953](https://github.com/spectreconsole/spectre.console/pull/953)
## What's changed?
* Add support for .NET 7.0 by [@patriksvensson](https://github.com/patriksvensson) in [#1056](https://github.com/spectreconsole/spectre.console/pull/1056)
* Add `Layout` widget by [@patriksvensson](https://github.com/patriksvensson) in [#1041](https://github.com/spectreconsole/spectre.console/pull/1041)
* Add JSON text renderer by [@patriksvensson](https://github.com/patriksvensson) in [#1086](https://github.com/spectreconsole/spectre.console/pull/1086)
* Backward direction of text prompt autocomplete by [@nkochnev](https://github.com/nkochnev) in [#921](https://github.com/spectreconsole/spectre.console/pull/921)
* Custom mask for secret by [@GaryMcD](https://github.com/GaryMcD) in [#970](https://github.com/spectreconsole/spectre.console/pull/970)
* Allow selections to wrap around by [@Saalvage](https://github.com/Saalvage) in [#976](https://github.com/spectreconsole/spectre.console/pull/976)
* Join .NET Foundation by [@patriksvensson](https://github.com/patriksvensson) in [#978](https://github.com/spectreconsole/spectre.console/pull/978)
* Adding value: a single semi-colon! by [@johanlindfors](https://github.com/johanlindfors) in [#986](https://github.com/spectreconsole/spectre.console/pull/986)
* Fix `@` being used in Figlet font by [@Saalvage](https://github.com/Saalvage) in [#972](https://github.com/spectreconsole/spectre.console/pull/972)
* Add new and transferred issues to backlog project by [@patriksvensson](https://github.com/patriksvensson) in [#995](https://github.com/spectreconsole/spectre.console/pull/995)
* Pin SDK due to a bug in .NET 6.0.401 by [@patriksvensson](https://github.com/patriksvensson) in [#1011](https://github.com/spectreconsole/spectre.console/pull/1011)
* Remove period trimming by [@BenjaminMichaelis](https://github.com/BenjaminMichaelis) in [#1008](https://github.com/spectreconsole/spectre.console/pull/1008)
* Allow `PACKET` key on MultiSelectionPrompt by [@nilaoda](https://github.com/nilaoda) in [#1012](https://github.com/spectreconsole/spectre.console/pull/1012)
* Added Suckless Simple Terminal to list of ANSI terminals by [@picture](https://github.com/picture)-vision in [#1013](https://github.com/spectreconsole/spectre.console/pull/1013)
* Add culture option to `TypeConverterHelper`, `TextPrompt` and `AnsiConsole` by [@sowa](https://github.com/sowa)705 in [#1014](https://github.com/spectreconsole/spectre.console/pull/1014)
* Minor typo fixes by [@ardalis](https://github.com/ardalis) in [#1021](https://github.com/spectreconsole/spectre.console/pull/1021)
* Alignment fixes by [@patriksvensson](https://github.com/patriksvensson) in [#1066](https://github.com/spectreconsole/spectre.console/pull/1066)
* `IndexOf` replaced by Count at Add method - Performance issue #975 fixed by [@maije](https://github.com/maije) in [#1096](https://github.com/spectreconsole/spectre.console/pull/1096)
* Modified tokenizer not to break on on `]]]` at the end of a style by [@nils](https://github.com/nils)-a in [#1027](https://github.com/spectreconsole/spectre.console/pull/1027)
* Command line argument parsing improvements by [@FrankRay](https://github.com/FrankRay)78 in [#1048](https://github.com/spectreconsole/spectre.console/pull/1048)
* Show help for default command by [@krisrok](https://github.com/krisrok) in [#953](https://github.com/spectreconsole/spectre.console/pull/953)
* Automatically display default values of options in the help page by @0xced in [#1032](https://github.com/spectreconsole/spectre.console/pull/1032)
## Documentation updates
* Add link to documentation in README by [@ardalis](https://github.com/ardalis) in [#1030](https://github.com/spectreconsole/spectre.console/pull/1030)
* Update `.NET 5` references in docs by [@eduherminio](https://github.com/eduherminio) in [#964](https://github.com/spectreconsole/spectre.console/pull/964)
* Blog date fix by [@phil](https://github.com/phil)-scott-78 in [#963](https://github.com/spectreconsole/spectre.console/pull/963)
* Update sponsors by [@tomkerkhove](https://github.com/tomkerkhove) in [#1089](https://github.com/spectreconsole/spectre.console/pull/1089)
* Inline `CommandArgument` required/optional style in template parameter docs by [@ArveSystad](https://github.com/ArveSystad) in [#1090](https://github.com/spectreconsole/spectre.console/pull/1090)
* Add documentation for `BreakdownChart` by [@BenjaminMichaelis](https://github.com/BenjaminMichaelis) in [#1000](https://github.com/spectreconsole/spectre.console/pull/1000)
* Create `Panel` documentation by [@patrickfreilinger](https://github.com/patrickfreilinger) in [#1016](https://github.com/spectreconsole/spectre.console/pull/1016)
* Added details for using links within markup. by [@GaryMcD](https://github.com/GaryMcD) in [#961](https://github.com/spectreconsole/spectre.console/pull/961)
* Added documentation for `Rows` widget by [@Elisha](https://github.com/Elisha)-Aguilera in [#1038](https://github.com/spectreconsole/spectre.console/pull/1038)
* Added documentation guide for `Grid` widget by [@Elisha](https://github.com/Elisha)-Aguilera in [#1043](https://github.com/spectreconsole/spectre.console/pull/1043)
* Added documentation guide for the `Padder` widget by [@Elisha](https://github.com/Elisha)-Aguilera in [#1046](https://github.com/spectreconsole/spectre.console/pull/1046)
* Created a `Columns` widget documentation by [@wguner](https://github.com/wguner) in [#1044](https://github.com/spectreconsole/spectre.console/pull/1044)
* Fixed typo in `Panel` documentation [@bcwood](https://github.com/bcwood) in [#1068](https://github.com/spectreconsole/spectre.console/pull/1068)
* Clarified the license for `SixLabors.ImageSharp` by [@FrankRay](https://github.com/FrankRay)78 in [#1073](https://github.com/spectreconsole/spectre.console/pull/1073)
* Add documentation for `Layout` by [@patriksvensson](https://github.com/patriksvensson) in [#1127](https://github.com/spectreconsole/spectre.console/pull/1127)
## Dependencies
* Update dependency `Wcwidth.Sources` to `v1` by [@renovate](https://github.com/renovate) in [#969](https://github.com/spectreconsole/spectre.console/pull/969)
* Update `actions/setup-dotnet` action to `v3` by [@renovate](https://github.com/renovate) in [#982](https://github.com/spectreconsole/spectre.console/pull/982)
* Update dependency `Microsoft.NET.Test.Sdk` to `v17.3.2` by [@renovate](https://github.com/renovate) in [#977](https://github.com/spectreconsole/spectre.console/pull/977)
* Update dependency `cake.tool` to `v2.3.0` by [@renovate](https://github.com/renovate) in [#1015](https://github.com/spectreconsole/spectre.console/pull/1015)

View File

@@ -37,8 +37,8 @@ app.Configure(config =>
config.AddCommand<HelloCommand>("hello") config.AddCommand<HelloCommand>("hello")
.WithAlias("hola") .WithAlias("hola")
.WithDescription("Say hello") .WithDescription("Say hello")
.WithExample(new []{"hello", "Phil"}) .WithExample("hello", "Phil")
.WithExample(new []{"hello", "Phil", "--count", "4"}); .WithExample("hello", "Phil", "--count", "4");
}); });
``` ```

View File

@@ -29,13 +29,13 @@ var fruits = AnsiConsole.Prompt(
"[grey](Press [blue]<space>[/] to toggle a fruit, " + "[grey](Press [blue]<space>[/] to toggle a fruit, " +
"[green]<enter>[/] to accept)[/]") "[green]<enter>[/] to accept)[/]")
.AddChoices(new[] { .AddChoices(new[] {
"Apple", "Apricot", "Avocado", "Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry", "Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut", "Cherry", "Cloudberry", "Coconut",
})); }));
// Write the selected fruits to the terminal // Write the selected fruits to the terminal
foreach (string fruit in fruits) foreach (string fruit in fruits)
{ {
AnsiConsole.WriteLine(fruit); AnsiConsole.WriteLine(fruit);
} }

View File

@@ -12,7 +12,7 @@ Spectre.Console can render [FIGlet](http://www.figlet.org/) text by using the `F
```csharp ```csharp
AnsiConsole.Write( AnsiConsole.Write(
new FigletText("Hello") new FigletText("Hello")
.LeftAligned() .LeftJustified()
.Color(Color.Red)); .Color(Color.Red));
``` ```
@@ -26,6 +26,6 @@ var font = FigletFont.Load("starwars.flf");
AnsiConsole.Write( AnsiConsole.Write(
new FigletText(font, "Hello") new FigletText(font, "Hello")
.LeftAligned() .LeftJustified()
.Color(Color.Red)); .Color(Color.Red));
``` ```

View File

@@ -45,16 +45,16 @@ grid.AddColumn();
// Add header row // Add header row
grid.AddRow(new Text[]{ grid.AddRow(new Text[]{
new Text("Header 1", new Style(Color.Red, Color.Black)).LeftAligned(), new Text("Header 1", new Style(Color.Red, Color.Black)).LeftJustified(),
new Text("Header 2", new Style(Color.Green, Color.Black)).Centered(), new Text("Header 2", new Style(Color.Green, Color.Black)).Centered(),
new Text("Header 3", new Style(Color.Blue, Color.Black)).RightAligned() new Text("Header 3", new Style(Color.Blue, Color.Black)).RightJustified()
}); });
// Add content row // Add content row
grid.AddRow(new Text[]{ grid.AddRow(new Text[]{
new Text("Row 1").LeftAligned(), new Text("Row 1").LeftJustified(),
new Text("Row 2").Centered(), new Text("Row 2").Centered(),
new Text("Row 3").RightAligned() new Text("Row 3").RightJustified()
}); });
// Write centered cell grid contents to Console // Write centered cell grid contents to Console
@@ -73,9 +73,9 @@ grid.AddColumn();
// Add header row // Add header row
grid.AddRow(new Text[]{ grid.AddRow(new Text[]{
new Text("Header 1", new Style(Color.Red, Color.Black)).LeftAligned(), new Text("Header 1", new Style(Color.Red, Color.Black)).LeftJustified(),
new Text("Header 2", new Style(Color.Green, Color.Black)).Centered(), new Text("Header 2", new Style(Color.Green, Color.Black)).Centered(),
new Text("Header 3", new Style(Color.Blue, Color.Black)).RightAligned() new Text("Header 3", new Style(Color.Blue, Color.Black)).RightJustified()
}); });
var embedded = new Grid(); var embedded = new Grid();
@@ -88,7 +88,7 @@ embedded.AddRow(new Text("Embedded III"), new Text("Embedded IV"));
// Add content row // Add content row
grid.AddRow( grid.AddRow(
new Text("Row 1").LeftAligned(), new Text("Row 1").LeftJustified(),
new Text("Row 2").Centered(), new Text("Row 2").Centered(),
embedded embedded
); );

View File

@@ -53,7 +53,7 @@ You can also specify it via an extension method:
```csharp ```csharp
var rule = new Rule("[red]Hello[/]"); var rule = new Rule("[red]Hello[/]");
rule.LeftAligned(); rule.LeftJustified();
AnsiConsole.Write(rule); AnsiConsole.Write(rule);
``` ```

View File

@@ -34,7 +34,7 @@ You can also specify styles via extension methods:
```csharp ```csharp
var path = new TextPath("C:/This/Path/Is/Too/Long/To/Fit/In/The/Area.txt") var path = new TextPath("C:/This/Path/Is/Too/Long/To/Fit/In/The/Area.txt")
.RightAligned(); .RightJustified();
``` ```
## Styling ## Styling

View File

@@ -115,7 +115,7 @@ namespace Prompt
.AddChoices(favorites)); .AddChoices(favorites));
} }
AnsiConsole.MarkupLine("Your selected: [yellow]{0}[/]", fruit); AnsiConsole.MarkupLine("You selected: [yellow]{0}[/]", fruit);
return fruit; return fruit;
} }

View File

@@ -16,11 +16,15 @@ public class FavorInstanceAnsiConsoleOverStaticAnalyzer : SpectreAnalyzer
/// <inheritdoc /> /// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext) protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{ {
var ansiConsoleType = compilationStartContext.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
if (ansiConsoleType == null)
{
return;
}
compilationStartContext.RegisterOperationAction( compilationStartContext.RegisterOperationAction(
context => context =>
{ {
var ansiConsoleType = context.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
// if this operation isn't an invocation against one of the System.Console methods // if this operation isn't an invocation against one of the System.Console methods
// defined in _methods then we can safely stop analyzing and return; // defined in _methods then we can safely stop analyzing and return;
var invocationOperation = (IInvocationOperation)context.Operation; var invocationOperation = (IInvocationOperation)context.Operation;

View File

@@ -17,6 +17,16 @@ public class NoConcurrentLiveRenderablesAnalyzer : SpectreAnalyzer
/// <inheritdoc /> /// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext) protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{ {
var liveTypes = Constants.LiveRenderables
.Select(i => compilationStartContext.Compilation.GetTypeByMetadataName(i))
.Where(i => i != null)
.ToImmutableArray();
if (liveTypes.Length == 0)
{
return;
}
compilationStartContext.RegisterOperationAction( compilationStartContext.RegisterOperationAction(
context => context =>
{ {
@@ -29,27 +39,21 @@ public class NoConcurrentLiveRenderablesAnalyzer : SpectreAnalyzer
return; return;
} }
var liveTypes = Constants.LiveRenderables
.Select(i => context.Compilation.GetTypeByMetadataName(i))
.ToImmutableArray();
if (liveTypes.All(i => !SymbolEqualityComparer.Default.Equals(i, methodSymbol.ContainingType))) if (liveTypes.All(i => !SymbolEqualityComparer.Default.Equals(i, methodSymbol.ContainingType)))
{ {
return; return;
} }
#pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer var model = context.Operation.SemanticModel!;
var model = context.Compilation.GetSemanticModel(context.Operation.Syntax.SyntaxTree);
#pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
var parentInvocations = invocationOperation var parentInvocations = invocationOperation
.Syntax.Ancestors() .Syntax.Ancestors()
.OfType<InvocationExpressionSyntax>() .OfType<InvocationExpressionSyntax>()
.Select(i => model.GetOperation(i)) .Select(i => model.GetOperation(i, context.CancellationToken))
.OfType<IInvocationOperation>() .OfType<IInvocationOperation>()
.ToList(); .ToList();
if (parentInvocations.All(parent => if (parentInvocations.All(parent =>
parent.TargetMethod.Name != StartMethod || !liveTypes.Contains(parent.TargetMethod.ContainingType))) parent.TargetMethod.Name != StartMethod || !liveTypes.Contains(parent.TargetMethod.ContainingType, SymbolEqualityComparer.Default)))
{ {
return; return;
} }

View File

@@ -17,6 +17,14 @@ public class NoPromptsDuringLiveRenderablesAnalyzer : SpectreAnalyzer
/// <inheritdoc /> /// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext) protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{ {
var ansiConsoleType = compilationStartContext.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
var ansiConsoleExtensionsType = compilationStartContext.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsoleExtensions");
if (ansiConsoleType is null && ansiConsoleExtensionsType is null)
{
return;
}
compilationStartContext.RegisterOperationAction( compilationStartContext.RegisterOperationAction(
context => context =>
{ {
@@ -31,22 +39,17 @@ public class NoPromptsDuringLiveRenderablesAnalyzer : SpectreAnalyzer
return; return;
} }
var ansiConsoleType = context.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
var ansiConsoleExtensionsType = context.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsoleExtensions");
if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, ansiConsoleType) && if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, ansiConsoleType) &&
!SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, ansiConsoleExtensionsType)) !SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, ansiConsoleExtensionsType))
{ {
return; return;
} }
#pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer var model = context.Operation.SemanticModel!;
var model = context.Compilation.GetSemanticModel(context.Operation.Syntax.SyntaxTree);
#pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
var parentInvocations = invocationOperation var parentInvocations = invocationOperation
.Syntax.Ancestors() .Syntax.Ancestors()
.OfType<InvocationExpressionSyntax>() .OfType<InvocationExpressionSyntax>()
.Select(i => model.GetOperation(i)) .Select(i => model.GetOperation(i, context.CancellationToken))
.OfType<IInvocationOperation>() .OfType<IInvocationOperation>()
.ToList(); .ToList();
@@ -56,7 +59,7 @@ public class NoPromptsDuringLiveRenderablesAnalyzer : SpectreAnalyzer
if (parentInvocations.All(parent => if (parentInvocations.All(parent =>
parent.TargetMethod.Name != "Start" || parent.TargetMethod.Name != "Start" ||
!liveTypes.Contains(parent.TargetMethod.ContainingType))) !liveTypes.Contains(parent.TargetMethod.ContainingType, SymbolEqualityComparer.Default)))
{ {
return; return;
} }

View File

@@ -18,6 +18,13 @@ public class UseSpectreInsteadOfSystemConsoleAnalyzer : SpectreAnalyzer
/// <inheritdoc /> /// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext) protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{ {
var systemConsoleType = compilationStartContext.Compilation.GetTypeByMetadataName("System.Console");
var spectreConsoleType = compilationStartContext.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
if (systemConsoleType == null || spectreConsoleType == null)
{
return;
}
compilationStartContext.RegisterOperationAction( compilationStartContext.RegisterOperationAction(
context => context =>
{ {
@@ -31,8 +38,6 @@ public class UseSpectreInsteadOfSystemConsoleAnalyzer : SpectreAnalyzer
return; return;
} }
var systemConsoleType = context.Compilation.GetTypeByMetadataName("System.Console");
if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.ContainingType, systemConsoleType)) if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.ContainingType, systemConsoleType))
{ {
return; return;

View File

@@ -1,4 +1,5 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification;
namespace Spectre.Console.Analyzer.CodeActions; namespace Spectre.Console.Analyzer.CodeActions;
@@ -31,83 +32,171 @@ public class SwitchToAnsiConsoleAction : CodeAction
/// <inheritdoc /> /// <inheritdoc />
protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{ {
var originalCaller = ((MemberAccessExpressionSyntax)_originalInvocation.Expression).Name.ToString(); var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var compilation = editor.SemanticModel.Compilation;
var syntaxTree = await _document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (syntaxTree == null) var operation = editor.SemanticModel.GetOperation(_originalInvocation, cancellationToken) as IInvocationOperation;
{ if (operation == null)
return _document; {
} return _document;
}
var root = (CompilationUnitSyntax)await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
// If there is an IAnsiConsole passed into the method then we'll use it.
// If there is an ansiConsole passed into the method then we'll use it. // otherwise we'll check for a field level instance.
// otherwise we'll check for a field level instance. // if neither of those exist we'll fall back to the static param.
// if neither of those exist we'll fall back to the static param. var spectreConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
var ansiConsoleParameterDeclaration = GetAnsiConsoleParameterDeclaration(); var iansiConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.IAnsiConsole");
var ansiConsoleFieldIdentifier = GetAnsiConsoleFieldDeclaration();
var ansiConsoleIdentifier = ansiConsoleParameterDeclaration ?? ISymbol? accessibleConsoleSymbol = spectreConsoleSymbol;
ansiConsoleFieldIdentifier ?? if (iansiConsoleSymbol != null)
Constants.StaticInstance; {
var isInStaticContext = IsInStaticContext(operation, cancellationToken, out var parentStaticMemberStartPosition);
// Replace the System.Console call with a call to the identifier above.
var newRoot = root.ReplaceNode( foreach (var symbol in editor.SemanticModel.LookupSymbols(operation.Syntax.GetLocation().SourceSpan.Start))
_originalInvocation, {
GetImportedSpectreCall(originalCaller, ansiConsoleIdentifier)); // LookupSymbols check the accessibility of the symbol, but it can
// suggest instance members when the current context is static.
// If we are calling the static instance and Spectre isn't imported yet we should do so. var symbolType = symbol switch
if (ansiConsoleIdentifier == Constants.StaticInstance && root.Usings.ToList().All(i => i.Name.ToString() != Constants.SpectreConsole)) {
{ IParameterSymbol parameter => parameter.Type,
newRoot = newRoot.AddUsings(Syntax.SpectreUsing); IFieldSymbol field when !isInStaticContext || field.IsStatic => field.Type,
} IPropertySymbol { GetMethod: not null } property when !isInStaticContext || property.IsStatic => property.Type,
ILocalSymbol local => local.Type,
return _document.WithSyntaxRoot(newRoot); _ => null,
} };
private string? GetAnsiConsoleParameterDeclaration() // Locals can be returned even if there are not valid in the current context. For instance,
{ // it can return locals declared after the current location. Or it can return locals that
return _originalInvocation // should not be accessible in a static local function.
.Ancestors().OfType<MethodDeclarationSyntax>() //
.First() // void Sample()
.ParameterList.Parameters // {
.FirstOrDefault(i => i.Type?.NormalizeWhitespace()?.ToString() == "IAnsiConsole") // int local = 0;
?.Identifier.Text; // static void LocalFunction() => local; <-- local is invalid here but LookupSymbols suggests it
} // }
//
private string? GetAnsiConsoleFieldDeclaration() // Parameters from the ancestor methods or local functions are also returned even if the operation is in a static local function.
{ if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter)
// let's look to see if our call is in a static method. {
// if so we'll only want to look for static IAnsiConsoles var localPosition = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken).GetLocation().SourceSpan.Start;
// and vice-versa if we aren't.
var isStatic = _originalInvocation // The local is not part of the source tree
.Ancestors() if (localPosition == null)
.OfType<MethodDeclarationSyntax>() {
.First() break;
.Modifiers.Any(i => i.IsKind(SyntaxKind.StaticKeyword)); }
return _originalInvocation // The local is declared after the current expression
.Ancestors().OfType<ClassDeclarationSyntax>() if (localPosition > _originalInvocation.Span.Start)
.First() {
.Members break;
.OfType<FieldDeclarationSyntax>() }
.FirstOrDefault(i =>
i.Declaration.Type.NormalizeWhitespace().ToString() == "IAnsiConsole" && // The local is declared outside the static local function
(!isStatic ^ i.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword)))) if (isInStaticContext && localPosition < parentStaticMemberStartPosition)
?.Declaration.Variables.First().Identifier.Text; {
} break;
}
private ExpressionSyntax GetImportedSpectreCall(string originalCaller, string ansiConsoleIdentifier) }
{
return ExpressionStatement( if (IsOrImplementSymbol(symbolType, iansiConsoleSymbol))
InvocationExpression( {
MemberAccessExpression( accessibleConsoleSymbol = symbol;
SyntaxKind.SimpleMemberAccessExpression, break;
IdentifierName(ansiConsoleIdentifier), }
IdentifierName(originalCaller))) }
.WithArgumentList(_originalInvocation.ArgumentList) }
.WithTrailingTrivia(_originalInvocation.GetTrailingTrivia())
.WithLeadingTrivia(_originalInvocation.GetLeadingTrivia())) if (accessibleConsoleSymbol == null)
.Expression; {
return _document;
}
// Replace the original invocation
var generator = editor.Generator;
var consoleExpression = accessibleConsoleSymbol switch
{
ITypeSymbol typeSymbol => generator.TypeExpression(typeSymbol, addImport: true).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation),
_ => generator.IdentifierName(accessibleConsoleSymbol.Name),
};
var newExpression = generator.InvocationExpression(generator.MemberAccessExpression(consoleExpression, operation.TargetMethod.Name), _originalInvocation.ArgumentList.Arguments)
.WithLeadingTrivia(_originalInvocation.GetLeadingTrivia())
.WithTrailingTrivia(_originalInvocation.GetTrailingTrivia());
editor.ReplaceNode(_originalInvocation, newExpression);
return editor.GetChangedDocument();
}
private static bool IsOrImplementSymbol(ITypeSymbol? symbol, ITypeSymbol interfaceSymbol)
{
if (symbol == null)
{
return false;
}
if (SymbolEqualityComparer.Default.Equals(symbol, interfaceSymbol))
{
return true;
}
foreach (var iface in symbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, interfaceSymbol))
{
return true;
}
}
return false;
}
private static bool IsInStaticContext(IOperation operation, CancellationToken cancellationToken, out int parentStaticMemberStartPosition)
{
// Local functions can be nested, and an instance local function can be declared
// in a static local function. So, you need to continue to check ancestors when a
// local function is not static.
foreach (var member in operation.Syntax.Ancestors())
{
if (member is LocalFunctionStatementSyntax localFunction)
{
var symbol = operation.SemanticModel!.GetDeclaredSymbol(localFunction, cancellationToken);
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = localFunction.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is LambdaExpressionSyntax lambdaExpression)
{
var symbol = operation.SemanticModel!.GetSymbolInfo(lambdaExpression, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = lambdaExpression.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is AnonymousMethodExpressionSyntax anonymousMethod)
{
var symbol = operation.SemanticModel!.GetSymbolInfo(anonymousMethod, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = anonymousMethod.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is MethodDeclarationSyntax methodDeclaration)
{
parentStaticMemberStartPosition = methodDeclaration.GetLocation().SourceSpan.Start;
var symbol = operation.SemanticModel!.GetDeclaredSymbol(methodDeclaration, cancellationToken);
return symbol != null && symbol.IsStatic;
}
}
parentStaticMemberStartPosition = -1;
return false;
} }
} }

View File

@@ -20,7 +20,7 @@ public class StaticAnsiConsoleToInstanceFix : CodeFixProvider
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root != null) if (root != null)
{ {
var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>(); var methodDeclaration = root.FindNode(context.Span, getInnermostNodeForTie: true).FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (methodDeclaration != null) if (methodDeclaration != null)
{ {
context.RegisterCodeFix( context.RegisterCodeFix(

View File

@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
/// <summary> /// <summary>
@@ -39,10 +41,11 @@ public sealed class CommandApp : ICommandApp
/// Sets the default command. /// Sets the default command.
/// </summary> /// </summary>
/// <typeparam name="TCommand">The command type.</typeparam> /// <typeparam name="TCommand">The command type.</typeparam>
public void SetDefaultCommand<TCommand>() /// <returns>A <see cref="DefaultCommandConfigurator"/> that can be used to configure the default command.</returns>
public DefaultCommandConfigurator SetDefaultCommand<TCommand>()
where TCommand : class, ICommand where TCommand : class, ICommand
{ {
GetConfigurator().SetDefaultCommand<TCommand>(); return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand<TCommand>());
} }
/// <summary> /// <summary>

View File

@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
/// <summary> /// <summary>
@@ -8,6 +10,7 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
where TDefaultCommand : class, ICommand where TDefaultCommand : class, ICommand
{ {
private readonly CommandApp _app; private readonly CommandApp _app;
private readonly DefaultCommandConfigurator _defaultCommandConfigurator;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CommandApp{TDefaultCommand}"/> class. /// Initializes a new instance of the <see cref="CommandApp{TDefaultCommand}"/> class.
@@ -16,7 +19,7 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
public CommandApp(ITypeRegistrar? registrar = null) public CommandApp(ITypeRegistrar? registrar = null)
{ {
_app = new CommandApp(registrar); _app = new CommandApp(registrar);
_app.GetConfigurator().SetDefaultCommand<TDefaultCommand>(); _defaultCommandConfigurator = _app.SetDefaultCommand<TDefaultCommand>();
} }
/// <summary> /// <summary>
@@ -46,5 +49,32 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
public Task<int> RunAsync(IEnumerable<string> args) public Task<int> RunAsync(IEnumerable<string> args)
{ {
return _app.RunAsync(args); return _app.RunAsync(args);
}
internal Configurator GetConfigurator()
{
return _app.GetConfigurator();
}
/// <summary>
/// Sets the description of the default command.
/// </summary>
/// <param name="description">The default command description.</param>
/// <returns>The same <see cref="CommandApp{TDefaultCommand}"/> instance so that multiple calls can be chained.</returns>
public CommandApp<TDefaultCommand> WithDescription(string description)
{
_defaultCommandConfigurator.WithDescription(description);
return this;
}
/// <summary>
/// Sets data that will be passed to the command via the <see cref="CommandContext"/>.
/// </summary>
/// <param name="data">The data to pass to the default command.</param>
/// <returns>The same <see cref="CommandApp{TDefaultCommand}"/> instance so that multiple calls can be chained.</returns>
public CommandApp<TDefaultCommand> WithData(object data)
{
_defaultCommandConfigurator.WithData(data);
return this;
} }
} }

View File

@@ -42,6 +42,13 @@ public class CommandRuntimeException : CommandAppException
return new CommandRuntimeException($"Could not find converter for type '{parameter.ParameterType.FullName}'."); return new CommandRuntimeException($"Could not find converter for type '{parameter.ParameterType.FullName}'.");
} }
internal static CommandRuntimeException ConversionFailed(MappedCommandParameter parameter, TypeConverter typeConverter, Exception exception)
{
var standardValues = typeConverter.GetStandardValuesSupported() ? typeConverter.GetStandardValues() : null;
var validValues = standardValues == null ? string.Empty : $" Valid values are '{string.Join("', '", standardValues.Cast<object>().Select(Convert.ToString))}'";
return new CommandRuntimeException($"Failed to convert '{parameter.Value}' to {parameter.Parameter.ParameterType.Name}.{validValues}", exception);
}
internal static CommandRuntimeException ValidationFailed(ValidationResult result) internal static CommandRuntimeException ValidationFailed(ValidationResult result)
{ {
return new CommandRuntimeException(result.Message ?? "Unknown validation error."); return new CommandRuntimeException(result.Message ?? "Unknown validation error.");

View File

@@ -88,12 +88,12 @@ public static class ConfiguratorExtensions
configurator.Settings.StrictParsing = true; configurator.Settings.StrictParsing = true;
return configurator; return configurator;
} }
/// <summary> /// <summary>
/// Tells the help writer whether or not to trim trailing period. /// Tells the help writer whether or not to trim trailing period.
/// </summary> /// </summary>
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="trimTrailingPeriods">True to trim trailing period (default), false to not.</param> /// <param name="trimTrailingPeriods">True to trim trailing period (default), false to not.</param>
/// <returns>A configurator that can be used to configure the application further.</returns> /// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator TrimTrailingPeriods(this IConfigurator configurator, bool trimTrailingPeriods) public static IConfigurator TrimTrailingPeriods(this IConfigurator configurator, bool trimTrailingPeriods)
@@ -181,7 +181,8 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="name">The name of the command branch.</param> /// <param name="name">The name of the command branch.</param>
/// <param name="action">The command branch configuration.</param> /// <param name="action">The command branch configuration.</param>
public static void AddBranch( /// <returns>A branch configurator that can be used to configure the branch further.</returns>
public static IBranchConfigurator AddBranch(
this IConfigurator configurator, this IConfigurator configurator,
string name, string name,
Action<IConfigurator<CommandSettings>> action) Action<IConfigurator<CommandSettings>> action)
@@ -191,7 +192,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
configurator.AddBranch(name, action); return configurator.AddBranch(name, action);
} }
/// <summary> /// <summary>
@@ -201,7 +202,8 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="name">The name of the command branch.</param> /// <param name="name">The name of the command branch.</param>
/// <param name="action">The command branch configuration.</param> /// <param name="action">The command branch configuration.</param>
public static void AddBranch<TSettings>( /// <returns>A branch configurator that can be used to configure the branch further.</returns>
public static IBranchConfigurator AddBranch<TSettings>(
this IConfigurator<TSettings> configurator, this IConfigurator<TSettings> configurator,
string name, string name,
Action<IConfigurator<TSettings>> action) Action<IConfigurator<TSettings>> action)
@@ -212,7 +214,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
configurator.AddBranch(name, action); return configurator.AddBranch(name, action);
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,14 @@
namespace Spectre.Console.Cli;
/// <summary>
/// Represents a branch configurator.
/// </summary>
public interface IBranchConfigurator
{
/// <summary>
/// Adds an alias (an alternative name) to the branch being configured.
/// </summary>
/// <param name="name">The alias to add to the branch being configured.</param>
/// <returns>The same <see cref="IBranchConfigurator"/> instance so that multiple calls can be chained.</returns>
IBranchConfigurator WithAlias(string name);
}

View File

@@ -49,7 +49,15 @@ public interface ICommandAppSettings
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not parsing is strict. /// Gets or sets a value indicating whether or not parsing is strict.
/// </summary> /// </summary>
bool StrictParsing { get; set; } bool StrictParsing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not flags found on the commnd line
/// that would normally result in a <see cref="CommandParseException"/> being thrown
/// during parsing with the message "Flags cannot be assigned a value."
/// should instead be added to the remaining arguments collection.
/// </summary>
bool ConvertFlagsToRemainingArguments { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not exceptions should be propagated. /// Gets or sets a value indicating whether or not exceptions should be propagated.

View File

@@ -10,7 +10,7 @@ public interface ICommandConfigurator
/// </summary> /// </summary>
/// <param name="args">The example arguments.</param> /// <param name="args">The example arguments.</param>
/// <returns>The same <see cref="ICommandConfigurator"/> instance so that multiple calls can be chained.</returns> /// <returns>The same <see cref="ICommandConfigurator"/> instance so that multiple calls can be chained.</returns>
ICommandConfigurator WithExample(string[] args); ICommandConfigurator WithExample(params string[] args);
/// <summary> /// <summary>
/// Adds an alias (an alternative name) to the command being configured. /// Adds an alias (an alternative name) to the command being configured.

View File

@@ -14,7 +14,7 @@ public interface IConfigurator
/// Adds an example of how to use the application. /// Adds an example of how to use the application.
/// </summary> /// </summary>
/// <param name="args">The example arguments.</param> /// <param name="args">The example arguments.</param>
void AddExample(string[] args); void AddExample(params string[] args);
/// <summary> /// <summary>
/// Adds a command. /// Adds a command.
@@ -41,6 +41,7 @@ public interface IConfigurator
/// <typeparam name="TSettings">The command setting type.</typeparam> /// <typeparam name="TSettings">The command setting type.</typeparam>
/// <param name="name">The name of the command branch.</param> /// <param name="name">The name of the command branch.</param>
/// <param name="action">The command branch configurator.</param> /// <param name="action">The command branch configurator.</param>
void AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action) /// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action)
where TSettings : CommandSettings; where TSettings : CommandSettings;
} }

View File

@@ -19,6 +19,18 @@ public interface IConfigurator<in TSettings>
/// <param name="args">The example arguments.</param> /// <param name="args">The example arguments.</param>
void AddExample(string[] args); void AddExample(string[] args);
/// <summary>
/// Adds a default command.
/// </summary>
/// <remarks>
/// This is the command that will run if the user doesn't specify one on the command line.
/// It must be able to execute successfully by itself ie. without requiring any command line
/// arguments, flags or option values.
/// </remarks>
/// <typeparam name="TDefaultCommand">The default command type.</typeparam>
void SetDefaultCommand<TDefaultCommand>()
where TDefaultCommand : class, ICommandLimiter<TSettings>;
/// <summary> /// <summary>
/// Marks the branch as hidden. /// Marks the branch as hidden.
/// Hidden branches do not show up in help documentation or /// Hidden branches do not show up in help documentation or
@@ -51,6 +63,7 @@ public interface IConfigurator<in TSettings>
/// <typeparam name="TDerivedSettings">The derived command setting type.</typeparam> /// <typeparam name="TDerivedSettings">The derived command setting type.</typeparam>
/// <param name="name">The name of the command branch.</param> /// <param name="name">The name of the command branch.</param>
/// <param name="action">The command branch configuration.</param> /// <param name="action">The command branch configuration.</param>
void AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action) /// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
where TDerivedSettings : TSettings; where TDerivedSettings : TSettings;
} }

View File

@@ -65,6 +65,11 @@ internal sealed class CommandValueBinder
private object GetArray(CommandParameter parameter, object? value) private object GetArray(CommandParameter parameter, object? value)
{ {
if (value is Array)
{
return value;
}
// Add a new item to the array // Add a new item to the array
var array = (Array?)_lookup.GetValue(parameter); var array = (Array?)_lookup.GetValue(parameter);
Array newArray; Array newArray;

View File

@@ -78,14 +78,32 @@ internal static class CommandValueResolver
} }
else else
{ {
var converter = GetConverter(lookup, binder, resolver, mapped.Parameter); var (converter, stringConstructor) = GetConverter(lookup, binder, resolver, mapped.Parameter);
if (converter == null) if (converter == null)
{ {
throw CommandRuntimeException.NoConverterFound(mapped.Parameter); throw CommandRuntimeException.NoConverterFound(mapped.Parameter);
} }
object? value;
var mappedValue = mapped.Value ?? string.Empty;
try
{
try
{
value = converter.ConvertFromInvariantString(mappedValue);
}
catch (NotSupportedException) when (stringConstructor != null)
{
value = stringConstructor.Invoke(new object[] { mappedValue });
}
}
catch (Exception exception) when (exception is not CommandRuntimeException)
{
throw CommandRuntimeException.ConversionFailed(mapped, converter, exception);
}
// Assign the value to the parameter. // Assign the value to the parameter.
binder.Bind(mapped.Parameter, resolver, converter.ConvertFromInvariantString(mapped.Value ?? string.Empty)); binder.Bind(mapped.Parameter, resolver, value);
} }
} }
@@ -112,19 +130,45 @@ internal static class CommandValueResolver
{ {
if (result != null && result.GetType() != parameter.ParameterType) if (result != null && result.GetType() != parameter.ParameterType)
{ {
var converter = GetConverter(lookup, binder, resolver, parameter); var (converter, _) = GetConverter(lookup, binder, resolver, parameter);
if (converter != null) if (converter != null)
{ {
result = converter.ConvertFrom(result); result = result is Array array ? ConvertArray(array, converter) : converter.ConvertFrom(result);
} }
} }
return result; return result;
} }
[SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")] private static Array ConvertArray(Array sourceArray, TypeConverter converter)
private static TypeConverter? GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter)
{ {
Array? targetArray = null;
for (var i = 0; i < sourceArray.Length; i++)
{
var item = sourceArray.GetValue(i);
if (item != null)
{
var converted = converter.ConvertFrom(item);
if (converted != null)
{
targetArray ??= Array.CreateInstance(converted.GetType(), sourceArray.Length);
targetArray.SetValue(converted, i);
}
}
}
return targetArray ?? sourceArray;
}
[SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")]
private static (TypeConverter? Converter, ConstructorInfo? StringConstructor) GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter)
{
static ConstructorInfo? GetStringConstructor(Type type)
{
var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null);
return constructor?.GetParameters()[0].ParameterType == typeof(string) ? constructor : null;
}
if (parameter.Converter == null) if (parameter.Converter == null)
{ {
if (parameter.ParameterType.IsArray) if (parameter.ParameterType.IsArray)
@@ -136,12 +180,12 @@ internal static class CommandValueResolver
throw new InvalidOperationException("Could not get element type"); throw new InvalidOperationException("Could not get element type");
} }
return TypeDescriptor.GetConverter(elementType); return (TypeDescriptor.GetConverter(elementType), GetStringConstructor(elementType));
} }
if (parameter.IsFlagValue()) if (parameter.IsFlagValue())
{ {
// Is the optional value instanciated? // Is the optional value instantiated?
var value = lookup.GetValue(parameter) as IFlagValue; var value = lookup.GetValue(parameter) as IFlagValue;
if (value == null) if (value == null)
{ {
@@ -151,18 +195,18 @@ internal static class CommandValueResolver
value = lookup.GetValue(parameter) as IFlagValue; value = lookup.GetValue(parameter) as IFlagValue;
if (value == null) if (value == null)
{ {
throw new InvalidOperationException("Could not intialize optional value."); throw new InvalidOperationException("Could not initialize optional value.");
} }
} }
// Return a converter for the flag element type. // Return a converter for the flag element type.
return TypeDescriptor.GetConverter(value.Type); return (TypeDescriptor.GetConverter(value.Type), GetStringConstructor(value.Type));
} }
return TypeDescriptor.GetConverter(parameter.ParameterType); return (TypeDescriptor.GetConverter(parameter.ParameterType), GetStringConstructor(parameter.ParameterType));
} }
var type = Type.GetType(parameter.Converter.ConverterTypeName); var type = Type.GetType(parameter.Converter.ConverterTypeName);
return resolver.Resolve(type) as TypeConverter; return (resolver.Resolve(type) as TypeConverter, null);
} }
} }

View File

@@ -45,12 +45,10 @@ internal sealed class CommandExecutor
} }
// Parse and map the model against the arguments. // Parse and map the model against the arguments.
var parser = new CommandTreeParser(model, configuration.Settings); var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
var parsedResult = parser.Parse(args);
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
// Currently the root? // Currently the root?
if (parsedResult.Tree == null) if (parsedResult?.Tree == null)
{ {
// Display help. // Display help.
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues)); configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues));
@@ -75,6 +73,7 @@ internal sealed class CommandExecutor
} }
// Register the arguments with the container. // Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
_registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);
// Create the resolver and the context. // Create the resolver and the context.
@@ -86,6 +85,34 @@ internal sealed class CommandExecutor
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
} }
} }
private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
{
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
var parserContext = new CommandTreeParserContext(args, settings.ParsingMode);
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
var parsedResult = parser.Parse(parserContext, tokenizerResult);
var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand();
var lastParsedCommand = lastParsedLeaf?.Command;
if (lastParsedLeaf != null && lastParsedCommand != null &&
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
lastParsedCommand.DefaultCommand != null)
{
// Insert this branch's default command into the command line
// arguments and try again to see if it will parse.
var argsWithDefaultCommand = new List<string>(args);
argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name);
parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode);
tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand);
parsedResult = parser.Parse(parserContext, tokenizerResult);
}
return parsedResult;
}
private static string ResolveApplicationVersion(IConfiguration configuration) private static string ResolveApplicationVersion(IConfiguration configuration)
{ {

View File

@@ -0,0 +1,17 @@
namespace Spectre.Console.Cli;
internal sealed class BranchConfigurator : IBranchConfigurator
{
public ConfiguredCommand Command { get; }
public BranchConfigurator(ConfiguredCommand command)
{
Command = command;
}
public IBranchConfigurator WithAlias(string alias)
{
Command.Aliases.Add(alias);
return this;
}
}

View File

@@ -12,7 +12,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public bool PropagateExceptions { get; set; } public bool PropagateExceptions { get; set; }
public bool ValidateExamples { get; set; } public bool ValidateExamples { get; set; }
public bool TrimTrailingPeriod { get; set; } = true; public bool TrimTrailingPeriod { get; set; } = true;
public bool StrictParsing { get; set; } public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; } = false;
public ParsingMode ParsingMode => public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;

View File

@@ -9,7 +9,7 @@ internal sealed class CommandConfigurator : ICommandConfigurator
Command = command; Command = command;
} }
public ICommandConfigurator WithExample(string[] args) public ICommandConfigurator WithExample(params string[] args)
{ {
Command.Examples.Add(args); Command.Examples.Add(args);
return this; return this;

View File

@@ -20,22 +20,23 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
Examples = new List<string[]>(); Examples = new List<string[]>();
} }
public void AddExample(string[] args) public void AddExample(params string[] args)
{ {
Examples.Add(args); Examples.Add(args);
} }
public void SetDefaultCommand<TDefaultCommand>() public ConfiguredCommand SetDefaultCommand<TDefaultCommand>()
where TDefaultCommand : class, ICommand where TDefaultCommand : class, ICommand
{ {
DefaultCommand = ConfiguredCommand.FromType<TDefaultCommand>( DefaultCommand = ConfiguredCommand.FromType<TDefaultCommand>(
CliConstants.DefaultCommandName, isDefaultCommand: true); CliConstants.DefaultCommandName, isDefaultCommand: true);
return DefaultCommand;
} }
public ICommandConfigurator AddCommand<TCommand>(string name) public ICommandConfigurator AddCommand<TCommand>(string name)
where TCommand : class, ICommand where TCommand : class, ICommand
{ {
var command = Commands.AddAndReturn(ConfiguredCommand.FromType<TCommand>(name, false)); var command = Commands.AddAndReturn(ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false));
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }
@@ -47,12 +48,13 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }
public void AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action) public IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
var command = ConfiguredCommand.FromBranch<TSettings>(name); var command = ConfiguredCommand.FromBranch<TSettings>(name);
action(new Configurator<TSettings>(command, _registrar)); action(new Configurator<TSettings>(command, _registrar));
Commands.Add(command); var added = Commands.AddAndReturn(command);
return new BranchConfigurator(added);
} }
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command)
@@ -73,7 +75,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
return result; return result;
} }
void IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action) IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action)
{ {
var command = ConfiguredCommand.FromBranch(settings, name); var command = ConfiguredCommand.FromBranch(settings, name);
@@ -85,6 +87,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
} }
action(configurator); action(configurator);
Commands.Add(command); var added = Commands.AddAndReturn(command);
return new BranchConfigurator(added);
} }
} }

View File

@@ -1,90 +1,100 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings> internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings>
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
private readonly ConfiguredCommand _command; private readonly ConfiguredCommand _command;
private readonly ITypeRegistrar? _registrar; private readonly ITypeRegistrar? _registrar;
public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar) public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar)
{ {
_command = command; _command = command;
_registrar = registrar; _registrar = registrar;
} }
public void SetDescription(string description) public void SetDescription(string description)
{ {
_command.Description = description; _command.Description = description;
} }
public void AddExample(string[] args) public void AddExample(string[] args)
{ {
_command.Examples.Add(args); _command.Examples.Add(args);
} }
public void HideBranch() public void SetDefaultCommand<TDefaultCommand>()
{ where TDefaultCommand : class, ICommandLimiter<TSettings>
_command.IsHidden = true; {
} var defaultCommand = ConfiguredCommand.FromType<TDefaultCommand>(
CliConstants.DefaultCommandName, isDefaultCommand: true);
public ICommandConfigurator AddCommand<TCommand>(string name)
where TCommand : class, ICommandLimiter<TSettings> _command.Children.Add(defaultCommand);
{ }
var command = ConfiguredCommand.FromType<TCommand>(name);
var configurator = new CommandConfigurator(command); public void HideBranch()
{
_command.Children.Add(command); _command.IsHidden = true;
return configurator; }
}
public ICommandConfigurator AddCommand<TCommand>(string name)
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func) where TCommand : class, ICommandLimiter<TSettings>
where TDerivedSettings : TSettings {
{ var command = ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false);
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>( var configurator = new CommandConfigurator(command);
name, (context, settings) => func(context, (TDerivedSettings)settings));
_command.Children.Add(command);
_command.Children.Add(command); return configurator;
return new CommandConfigurator(command); }
}
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
public void AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action) where TDerivedSettings : TSettings
where TDerivedSettings : TSettings {
{ var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name); name, (context, settings) => func(context, (TDerivedSettings)settings));
action(new Configurator<TDerivedSettings>(command, _registrar));
_command.Children.Add(command); _command.Children.Add(command);
} return new CommandConfigurator(command);
}
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command)
{ public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
var method = GetType().GetMethod("AddCommand"); where TDerivedSettings : TSettings
if (method == null) {
{ var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
throw new CommandConfigurationException("Could not find AddCommand by reflection."); action(new Configurator<TDerivedSettings>(command, _registrar));
} var added = _command.Children.AddAndReturn(command);
return new BranchConfigurator(added);
method = method.MakeGenericMethod(command); }
if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command)
{ {
throw new CommandConfigurationException("Invoking AddCommand returned null."); var method = GetType().GetMethod("AddCommand");
} if (method == null)
{
return result; throw new CommandConfigurationException("Could not find AddCommand by reflection.");
} }
void IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action) method = method.MakeGenericMethod(command);
{
var command = ConfiguredCommand.FromBranch(settings, name); if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result))
{
// Create the configurator. throw new CommandConfigurationException("Invoking AddCommand returned null.");
var configuratorType = typeof(Configurator<>).MakeGenericType(settings); }
if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator))
{ return result;
throw new CommandConfigurationException("Could not create configurator by reflection."); }
}
IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action)
action(configurator); {
_command.Children.Add(command); var command = ConfiguredCommand.FromBranch(settings, name);
}
// Create the configurator.
var configuratorType = typeof(Configurator<>).MakeGenericType(settings);
if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator))
{
throw new CommandConfigurationException("Could not create configurator by reflection.");
}
action(configurator);
var added = _command.Children.AddAndReturn(command);
return new BranchConfigurator(added);
} }

View File

@@ -27,7 +27,10 @@ internal sealed class ConfiguredCommand
CommandType = commandType; CommandType = commandType;
SettingsType = settingsType; SettingsType = settingsType;
Delegate = @delegate; Delegate = @delegate;
IsDefaultCommand = isDefaultCommand; IsDefaultCommand = isDefaultCommand;
// Default commands are always created as hidden.
IsHidden = IsDefaultCommand;
Children = new List<ConfiguredCommand>(); Children = new List<ConfiguredCommand>();
Examples = new List<string[]>(); Examples = new List<string[]>();

View File

@@ -0,0 +1,36 @@
namespace Spectre.Console.Cli.Internal.Configuration;
/// <summary>
/// Fluent configurator for the default command.
/// </summary>
public sealed class DefaultCommandConfigurator
{
private readonly ConfiguredCommand _defaultCommand;
internal DefaultCommandConfigurator(ConfiguredCommand defaultCommand)
{
_defaultCommand = defaultCommand;
}
/// <summary>
/// Sets the description of the default command.
/// </summary>
/// <param name="description">The default command description.</param>
/// <returns>The same <see cref="DefaultCommandConfigurator"/> instance so that multiple calls can be chained.</returns>
public DefaultCommandConfigurator WithDescription(string description)
{
_defaultCommand.Description = description;
return this;
}
/// <summary>
/// Sets data that will be passed to the command via the <see cref="CommandContext"/>.
/// </summary>
/// <param name="data">The data to pass to the default command.</param>
/// <returns>The same <see cref="DefaultCommandConfigurator"/> instance so that multiple calls can be chained.</returns>
public DefaultCommandConfigurator WithData(object data)
{
_defaultCommand.Data = data;
return this;
}
}

View File

@@ -6,10 +6,6 @@ internal static class TypeRegistrarExtensions
{ {
var stack = new Stack<CommandInfo>(); var stack = new Stack<CommandInfo>();
model.Commands.ForEach(c => stack.Push(c)); model.Commands.ForEach(c => stack.Push(c));
if (model.DefaultCommand != null)
{
stack.Push(model.DefaultCommand);
}
while (stack.Count > 0) while (stack.Count > 0)
{ {

View File

@@ -348,7 +348,17 @@ internal static class HelpWriter
var columns = new List<string> { GetOptionParts(option) }; var columns = new List<string> { GetOptionParts(option) };
if (defaultValueColumn) if (defaultValueColumn)
{ {
columns.Add(option.DefaultValue == null ? " " : $"[bold]{option.DefaultValue.ToString().EscapeMarkup()}[/]"); static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]";
var defaultValue = option.DefaultValue switch
{
null => " ",
"" => " ",
Array { Length: 0 } => " ",
Array array => string.Join(", ", array.Cast<object>().Select(Bold)),
_ => Bold(option.DefaultValue),
};
columns.Add(defaultValue);
} }
columns.Add(option.Description?.TrimEnd('.') ?? " "); columns.Add(option.Description?.TrimEnd('.') ?? " ");

View File

@@ -1,7 +1,7 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class CommandInfo : ICommandContainer internal sealed class CommandInfo : ICommandContainer
{ {
public string Name { get; } public string Name { get; }
public HashSet<string> Aliases { get; } public HashSet<string> Aliases { get; }
public string? Description { get; } public string? Description { get; }
@@ -10,14 +10,17 @@ internal sealed class CommandInfo : ICommandContainer
public Type SettingsType { get; } public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, int>? Delegate { get; } public Func<CommandContext, CommandSettings, int>? Delegate { get; }
public bool IsDefaultCommand { get; } public bool IsDefaultCommand { get; }
public bool IsHidden { get; }
public CommandInfo? Parent { get; } public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; } public IList<CommandInfo> Children { get; }
public IList<CommandParameter> Parameters { get; } public IList<CommandParameter> Parameters { get; }
public IList<string[]> Examples { get; } public IList<string[]> Examples { get; }
public bool IsBranch => CommandType == null && Delegate == null; public bool IsBranch => CommandType == null && Delegate == null;
IList<CommandInfo> ICommandContainer.Commands => Children; IList<CommandInfo> ICommandContainer.Commands => Children;
// only branches can have a default command
public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null;
public bool IsHidden { get; }
public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{ {

View File

@@ -4,21 +4,20 @@ internal sealed class CommandModel : ICommandContainer
{ {
public string? ApplicationName { get; } public string? ApplicationName { get; }
public ParsingMode ParsingMode { get; } public ParsingMode ParsingMode { get; }
public CommandInfo? DefaultCommand { get; }
public IList<CommandInfo> Commands { get; } public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; } public IList<string[]> Examples { get; }
public bool TrimTrailingPeriod { get; } public bool TrimTrailingPeriod { get; }
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
public CommandModel( public CommandModel(
CommandAppSettings settings, CommandAppSettings settings,
CommandInfo? defaultCommand,
IEnumerable<CommandInfo> commands, IEnumerable<CommandInfo> commands,
IEnumerable<string[]> examples) IEnumerable<string[]> examples)
{ {
ApplicationName = settings.ApplicationName; ApplicationName = settings.ApplicationName;
ParsingMode = settings.ParsingMode; ParsingMode = settings.ParsingMode;
TrimTrailingPeriod = settings.TrimTrailingPeriod; TrimTrailingPeriod = settings.TrimTrailingPeriod;
DefaultCommand = defaultCommand;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>()); Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>()); Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
} }

View File

@@ -1,5 +1,5 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal static class CommandModelBuilder internal static class CommandModelBuilder
{ {
// Consider removing this in favor for value tuples at some point. // Consider removing this in favor for value tuples at some point.
@@ -25,18 +25,19 @@ internal static class CommandModelBuilder
result.Add(Build(null, command)); result.Add(Build(null, command));
} }
var defaultCommand = default(CommandInfo);
if (configuration.DefaultCommand != null) if (configuration.DefaultCommand != null)
{ {
// Add the examples from the configuration to the default command. // Add the examples from the configuration to the default command.
configuration.DefaultCommand.Examples.AddRange(configuration.Examples); configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// Build the default command. // Build the default command.
defaultCommand = Build(null, configuration.DefaultCommand); var defaultCommand = Build(null, configuration.DefaultCommand);
result.Add(defaultCommand);
} }
// Create the command model and validate it. // Create the command model and validate it.
var model = new CommandModel(configuration.Settings, defaultCommand, result, configuration.Examples); var model = new CommandModel(configuration.Settings, result, configuration.Examples);
CommandModelValidator.Validate(model, configuration.Settings); CommandModelValidator.Validate(model, configuration.Settings);
return model; return model;
@@ -54,7 +55,7 @@ internal static class CommandModelBuilder
foreach (var childCommand in command.Children) foreach (var childCommand in command.Children)
{ {
var child = Build(info, childCommand); var child = Build(info, childCommand);
info.Children.Add(child); info.Children.Add(child);
} }
// Normalize argument positions. // Normalize argument positions.

View File

@@ -14,7 +14,7 @@ internal static class CommandModelValidator
throw new ArgumentNullException(nameof(settings)); throw new ArgumentNullException(nameof(settings));
} }
if (model.Commands.Count == 0 && model.DefaultCommand == null) if (model.Commands.Count == 0)
{ {
throw CommandConfigurationException.NoCommandConfigured(); throw CommandConfigurationException.NoCommandConfigured();
} }
@@ -31,7 +31,6 @@ internal static class CommandModelValidator
} }
} }
Validate(model.DefaultCommand);
foreach (var command in model.Commands) foreach (var command in model.Commands)
{ {
Validate(command); Validate(command);
@@ -147,7 +146,7 @@ internal static class CommandModelValidator
{ {
try try
{ {
var parser = new CommandTreeParser(model, settings, ParsingMode.Strict); var parser = new CommandTreeParser(model, settings.CaseSensitivity, ParsingMode.Strict);
parser.Parse(example); parser.Parse(example);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -8,5 +8,13 @@ internal interface ICommandContainer
/// <summary> /// <summary>
/// Gets all commands in the container. /// Gets all commands in the container.
/// </summary> /// </summary>
IList<CommandInfo> Commands { get; } IList<CommandInfo> Commands { get; }
/// <summary>
/// Gets the default command for the container.
/// </summary>
/// <remarks>
/// Returns null if a default command has not been set.
/// </remarks>
CommandInfo? DefaultCommand { get; }
} }

View File

@@ -1,10 +1,13 @@
using static Spectre.Console.Cli.CommandTreeTokenizer;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal class CommandTreeParser internal class CommandTreeParser
{ {
private readonly CommandModel _configuration; private readonly CommandModel _configuration;
private readonly ParsingMode _parsingMode; private readonly ParsingMode _parsingMode;
private readonly CommandOptionAttribute _help; private readonly CommandOptionAttribute _help;
private readonly bool _convertFlagsToRemainingArguments;
public CaseSensitivity CaseSensitivity { get; } public CaseSensitivity CaseSensitivity { get; }
@@ -14,25 +17,26 @@ internal class CommandTreeParser
Remaining = 1, Remaining = 1,
} }
public CommandTreeParser(CommandModel configuration, ICommandAppSettings settings, ParsingMode? parsingMode = null) public CommandTreeParser(CommandModel configuration, CaseSensitivity caseSensitivity, ParsingMode? parsingMode = null, bool? convertFlagsToRemainingArguments = null)
{ {
if (settings is null) _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
{
throw new ArgumentNullException(nameof(settings));
}
_configuration = configuration;
_parsingMode = parsingMode ?? _configuration.ParsingMode; _parsingMode = parsingMode ?? _configuration.ParsingMode;
_help = new CommandOptionAttribute("-h|--help"); _help = new CommandOptionAttribute("-h|--help");
_convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false;
CaseSensitivity = settings.CaseSensitivity;
}
CaseSensitivity = caseSensitivity;
}
public CommandTreeParserResult Parse(IEnumerable<string> args) public CommandTreeParserResult Parse(IEnumerable<string> args)
{
var parserContext = new CommandTreeParserContext(args, _parsingMode);
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
return Parse(parserContext, tokenizerResult);
}
public CommandTreeParserResult Parse(CommandTreeParserContext context, CommandTreeTokenizerResult tokenizerResult)
{ {
var context = new CommandTreeParserContext(args, _parsingMode);
var tokenizerResult = CommandTreeTokenizer.Tokenize(context.Arguments);
var tokens = tokenizerResult.Tokens; var tokens = tokenizerResult.Tokens;
var rawRemaining = tokenizerResult.Remaining; var rawRemaining = tokenizerResult.Remaining;
@@ -254,10 +258,8 @@ internal class CommandTreeParser
// Find the option. // Find the option.
var option = node.FindOption(token.Value, isLongOption, CaseSensitivity); var option = node.FindOption(token.Value, isLongOption, CaseSensitivity);
if (option != null) if (option != null)
{ {
node.Mapped.Add(new MappedCommandParameter( ParseOptionValue(context, stream, token, node, option);
option, ParseOptionValue(context, stream, token, node, option)));
return; return;
} }
@@ -271,7 +273,7 @@ internal class CommandTreeParser
if (context.State == State.Remaining) if (context.State == State.Remaining)
{ {
ParseOptionValue(context, stream, token, node, null); ParseOptionValue(context, stream, token, node);
return; return;
} }
@@ -281,17 +283,19 @@ internal class CommandTreeParser
} }
else else
{ {
ParseOptionValue(context, stream, token, node, null); ParseOptionValue(context, stream, token, node);
} }
} }
private string? ParseOptionValue( private void ParseOptionValue(
CommandTreeParserContext context, CommandTreeParserContext context,
CommandTreeTokenStream stream, CommandTreeTokenStream stream,
CommandTreeToken token, CommandTreeToken token,
CommandTree current, CommandTree current,
CommandParameter? parameter) CommandParameter? parameter = null)
{ {
bool addToMappedCommandParameters = parameter != null;
var value = default(string); var value = default(string);
// Parse the value of the token (if any). // Parse the value of the token (if any).
@@ -325,7 +329,21 @@ internal class CommandTreeParser
else else
{ {
// Flags cannot be assigned a value. // Flags cannot be assigned a value.
throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); if (_convertFlagsToRemainingArguments)
{
value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
context.AddRemainingArgument(token.Value, value);
// Prevent the option and it's non-boolean value from being added to
// mapped parameters (otherwise an exception will be thrown later
// when binding the value to the flag in the comand settings)
addToMappedCommandParameters = false;
}
else
{
throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token);
}
} }
} }
else else
@@ -352,13 +370,14 @@ internal class CommandTreeParser
} }
} }
else else
{ {
context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null); context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null);
} }
} }
else else
{ {
if (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed) if (parameter == null && // Only add tokens which have not been matched to a command parameter
(context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed))
{ {
context.AddRemainingArgument(token.Value, null); context.AddRemainingArgument(token.Value, null);
} }
@@ -379,10 +398,12 @@ internal class CommandTreeParser
{ {
if (parameter.IsFlagValue()) if (parameter.IsFlagValue())
{ {
return null; value = null;
}
else
{
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
} }
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
} }
else else
{ {
@@ -394,6 +415,9 @@ internal class CommandTreeParser
} }
} }
return value; if (parameter != null && addToMappedCommandParameters)
{
current.Mapped.Add(new MappedCommandParameter(parameter, value));
}
} }
} }

View File

@@ -26,19 +26,16 @@ internal class CommandTreeParserContext
public void IncreaseArgumentPosition() public void IncreaseArgumentPosition()
{ {
CurrentArgumentPosition++; CurrentArgumentPosition++;
} }
public void AddRemainingArgument(string key, string? value) public void AddRemainingArgument(string key, string? value)
{ {
if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed) if (!_remaining.ContainsKey(key))
{ {
if (!_remaining.ContainsKey(key)) _remaining.Add(key, new List<string?>());
{ }
_remaining.Add(key, new List<string?>());
} _remaining[key].Add(value);
_remaining[key].Add(value);
}
} }
[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")] [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")]

View File

@@ -5,7 +5,8 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
private readonly List<CommandTreeToken> _tokens; private readonly List<CommandTreeToken> _tokens;
private int _position; private int _position;
public int Count => _tokens.Count; public int Count => _tokens.Count;
public int Position => _position;
public CommandTreeToken this[int index] => _tokens[index]; public CommandTreeToken this[int index] => _tokens[index];

View File

@@ -19,5 +19,6 @@ public interface IUnsafeConfigurator
/// <param name="name">The name of the command branch.</param> /// <param name="name">The name of the command branch.</param>
/// <param name="settings">The command setting type.</param> /// <param name="settings">The command setting type.</param>
/// <param name="action">The command branch configurator.</param> /// <param name="action">The command branch configurator.</param>
void AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action); /// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action);
} }

View File

@@ -89,15 +89,15 @@ public sealed class JsonText : JustInTimeRenderable
var context = new JsonBuilderContext(new JsonTextStyles var context = new JsonBuilderContext(new JsonTextStyles
{ {
BracesStyle = BracesStyle ?? new Style(Color.Grey), BracesStyle = BracesStyle ?? Color.Grey,
BracketsStyle = BracketsStyle ?? new Style(Color.Grey), BracketsStyle = BracketsStyle ?? Color.Grey,
MemberStyle = MemberStyle ?? new Style(Color.Blue), MemberStyle = MemberStyle ?? Color.Blue,
ColonStyle = ColonStyle ?? new Style(Color.Yellow), ColonStyle = ColonStyle ?? Color.Yellow,
CommaStyle = CommaStyle ?? new Style(Color.Grey), CommaStyle = CommaStyle ?? Color.Grey,
StringStyle = StringStyle ?? new Style(Color.Red), StringStyle = StringStyle ?? Color.Red,
NumberStyle = NumberStyle ?? new Style(Color.Green), NumberStyle = NumberStyle ?? Color.Green,
BooleanStyle = BooleanStyle ?? new Style(Color.Green), BooleanStyle = BooleanStyle ?? Color.Green,
NullStyle = NullStyle ?? new Style(Color.Grey), NullStyle = NullStyle ?? Color.Grey,
}); });
_syntax.Accept(JsonBuilder.Shared, context); _syntax.Accept(JsonBuilder.Shared, context);

View File

@@ -25,11 +25,25 @@ public sealed class CommandAppTester
/// <summary> /// <summary>
/// Sets the default command. /// Sets the default command.
/// </summary> /// </summary>
/// <param name="description">The optional default command description.</param>
/// <param name="data">The optional default command data.</param>
/// <typeparam name="T">The default command type.</typeparam> /// <typeparam name="T">The default command type.</typeparam>
public void SetDefaultCommand<T>() public void SetDefaultCommand<T>(string? description = null, object? data = null)
where T : class, ICommand where T : class, ICommand
{ {
_appConfiguration = (app) => app.SetDefaultCommand<T>(); _appConfiguration = (app) =>
{
var defaultCommandBuilder = app.SetDefaultCommand<T>();
if (description != null)
{
defaultCommandBuilder.WithDescription(description);
}
if (data != null)
{
defaultCommandBuilder.WithData(data);
}
};
} }
/// <summary> /// <summary>

View File

@@ -74,7 +74,7 @@ public static class CalendarExtensions
throw new ArgumentNullException(nameof(calendar)); throw new ArgumentNullException(nameof(calendar));
} }
calendar.HightlightStyle = style ?? Style.Plain; calendar.HighlightStyle = style ?? Style.Plain;
return calendar; return calendar;
} }

View File

@@ -7,24 +7,25 @@ namespace Spectre.Console;
internal static class AnsiDetector internal static class AnsiDetector
{ {
private static readonly Regex[] _regexes = new[] private static readonly Regex[] _regexes =
{ {
new Regex("^xterm"), // xterm, PuTTY, Mintty new("^xterm"), // xterm, PuTTY, Mintty
new Regex("^rxvt"), // RXVT new("^rxvt"), // RXVT
new Regex("^eterm"), // Eterm new("^eterm"), // Eterm
new Regex("^screen"), // GNU screen, tmux new("^screen"), // GNU screen, tmux
new Regex("tmux"), // tmux new("tmux"), // tmux
new Regex("^vt100"), // DEC VT series new("^vt100"), // DEC VT series
new Regex("^vt102"), // DEC VT series new("^vt102"), // DEC VT series
new Regex("^vt220"), // DEC VT series new("^vt220"), // DEC VT series
new Regex("^vt320"), // DEC VT series new("^vt320"), // DEC VT series
new Regex("ansi"), // ANSI new("ansi"), // ANSI
new Regex("scoansi"), // SCO ANSI new("scoansi"), // SCO ANSI
new Regex("cygwin"), // Cygwin, MinGW new("cygwin"), // Cygwin, MinGW
new Regex("linux"), // Linux console new("linux"), // Linux console
new Regex("konsole"), // Konsole new("konsole"), // Konsole
new Regex("bvterm"), // Bitvise SSH Client new("bvterm"), // Bitvise SSH Client
new Regex("^st-256color"), // Suckless Simple Terminal, st new("^st-256color"), // Suckless Simple Terminal, st
new("alacritty"), // Alacritty
}; };
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade) public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
@@ -61,7 +62,7 @@ internal static class AnsiDetector
return (false, true); return (false, true);
} }
internal static class Windows private static class Windows
{ {
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const int STD_OUTPUT_HANDLE = -11; private const int STD_OUTPUT_HANDLE = -11;

View File

@@ -11,7 +11,7 @@ public sealed class ElapsedTimeColumn : ProgressColumn
/// <summary> /// <summary>
/// Gets or sets the style of the remaining time text. /// Gets or sets the style of the remaining time text.
/// </summary> /// </summary>
public Style Style { get; set; } = new Style(foreground: Color.Blue); public Style Style { get; set; } = Color.Blue;
/// <inheritdoc/> /// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)

View File

@@ -13,7 +13,7 @@ public sealed class PercentageColumn : ProgressColumn
/// <summary> /// <summary>
/// Gets or sets the style for a completed task. /// Gets or sets the style for a completed task.
/// </summary> /// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green); public Style CompletedStyle { get; set; } = Color.Green;
/// <inheritdoc/> /// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)

View File

@@ -13,17 +13,17 @@ public sealed class ProgressBarColumn : ProgressColumn
/// <summary> /// <summary>
/// Gets or sets the style of completed portions of the progress bar. /// Gets or sets the style of completed portions of the progress bar.
/// </summary> /// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow); public Style CompletedStyle { get; set; } = Color.Yellow;
/// <summary> /// <summary>
/// Gets or sets the style of a finished progress bar. /// Gets or sets the style of a finished progress bar.
/// </summary> /// </summary>
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green); public Style FinishedStyle { get; set; } = Color.Green;
/// <summary> /// <summary>
/// Gets or sets the style of remaining portions of the progress bar. /// Gets or sets the style of remaining portions of the progress bar.
/// </summary> /// </summary>
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey); public Style RemainingStyle { get; set; } = Color.Grey;
/// <summary> /// <summary>
/// Gets or sets the style of an indeterminate progress bar. /// Gets or sets the style of an indeterminate progress bar.

View File

@@ -11,7 +11,7 @@ public sealed class RemainingTimeColumn : ProgressColumn
/// <summary> /// <summary>
/// Gets or sets the style of the remaining time text. /// Gets or sets the style of the remaining time text.
/// </summary> /// </summary>
public Style Style { get; set; } = new Style(foreground: Color.Blue); public Style Style { get; set; } = Color.Blue;
/// <inheritdoc/> /// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)

View File

@@ -74,7 +74,7 @@ public sealed class SpinnerColumn : ProgressColumn
/// <summary> /// <summary>
/// Gets or sets the style of the spinner. /// Gets or sets the style of the spinner.
/// </summary> /// </summary>
public Style? Style { get; set; } = new Style(foreground: Color.Yellow); public Style? Style { get; set; } = Color.Yellow;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SpinnerColumn"/> class. /// Initializes a new instance of the <see cref="SpinnerColumn"/> class.

View File

@@ -15,7 +15,7 @@ public sealed class Status
/// <summary> /// <summary>
/// Gets or sets the spinner style. /// Gets or sets the spinner style.
/// </summary> /// </summary>
public Style? SpinnerStyle { get; set; } = new Style(foreground: Color.Yellow); public Style? SpinnerStyle { get; set; } = Color.Yellow;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not status /// Gets or sets a value indicating whether or not status

View File

@@ -39,6 +39,15 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
/// </summary> /// </summary>
public bool ShowDefaultValue { get; set; } = true; public bool ShowDefaultValue { get; set; } = true;
/// <summary>
/// Gets or sets the string comparer to use when comparing user input
/// against Yes/No choices.
/// </summary>
/// <remarks>
/// Defaults to <see cref="StringComparer.CurrentCultureIgnoreCase"/>.
/// </remarks>
public StringComparer Comparer { get; set; } = StringComparer.CurrentCultureIgnoreCase;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ConfirmationPrompt"/> class. /// Initializes a new instance of the <see cref="ConfirmationPrompt"/> class.
/// </summary> /// </summary>
@@ -57,7 +66,9 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
/// <inheritdoc/> /// <inheritdoc/>
public async Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) public async Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
{ {
var prompt = new TextPrompt<char>(_prompt) var comparer = Comparer ?? StringComparer.CurrentCultureIgnoreCase;
var prompt = new TextPrompt<char>(_prompt, comparer)
.InvalidChoiceMessage(InvalidChoiceMessage) .InvalidChoiceMessage(InvalidChoiceMessage)
.ValidationErrorMessage(InvalidChoiceMessage) .ValidationErrorMessage(InvalidChoiceMessage)
.ShowChoices(ShowChoices) .ShowChoices(ShowChoices)
@@ -67,6 +78,7 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
.AddChoice(No); .AddChoice(No);
var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false); var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false);
return result == Yes;
return comparer.Compare(Yes.ToString(), result.ToString()) == 0;
} }
} }

View File

@@ -225,7 +225,7 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items) IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items)
{ {
var list = new List<IRenderable>(); var list = new List<IRenderable>();
var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue); var highlightStyle = HighlightStyle ?? Color.Blue;
if (Title != null) if (Title != null)
{ {

View File

@@ -137,8 +137,8 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items) IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items)
{ {
var list = new List<IRenderable>(); var list = new List<IRenderable>();
var disabledStyle = DisabledStyle ?? new Style(foreground: Color.Grey); var disabledStyle = DisabledStyle ?? Color.Grey;
var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue); var highlightStyle = HighlightStyle ?? Color.Blue;
if (Title != null) if (Title != null)
{ {

View File

@@ -132,6 +132,15 @@ public sealed class Style : IEquatable<Style>
return Parse(style); return Parse(style);
} }
/// <summary>
/// Implicitly converts <see cref="Color"/> into a <see cref="Style"/> with a foreground color.
/// </summary>
/// <param name="color">The foreground color.</param>
public static implicit operator Style(Color color)
{
return new Style(foreground: color);
}
/// <summary> /// <summary>
/// Converts the string representation of a style to its <see cref="Style"/> equivalent. /// Converts the string representation of a style to its <see cref="Style"/> equivalent.
/// </summary> /// </summary>

View File

@@ -82,7 +82,7 @@ public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorde
/// <summary> /// <summary>
/// Gets or sets the calendar's highlight <see cref="Style"/>. /// Gets or sets the calendar's highlight <see cref="Style"/>.
/// </summary> /// </summary>
public Style HightlightStyle public Style HighlightStyle
{ {
get => _highlightStyle; get => _highlightStyle;
set => MarkAsDirty(() => _highlightStyle = value); set => MarkAsDirty(() => _highlightStyle = value);
@@ -153,9 +153,9 @@ public sealed class Calendar : JustInTimeRenderable, IHasCulture, IHasTableBorde
_useSafeBorder = true; _useSafeBorder = true;
_borderStyle = null; _borderStyle = null;
_culture = CultureInfo.InvariantCulture; _culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue); _highlightStyle = Color.Blue;
_showHeader = true; _showHeader = true;
_calendarEvents = new ListWithCallback<CalendarEvent>(() => MarkAsDirty()); _calendarEvents = new ListWithCallback<CalendarEvent>(MarkAsDirty);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -13,27 +13,27 @@ public sealed class ExceptionStyle
/// <summary> /// <summary>
/// Gets or sets the exception color. /// Gets or sets the exception color.
/// </summary> /// </summary>
public Style Exception { get; set; } = new Style(Color.White); public Style Exception { get; set; } = Color.White;
/// <summary> /// <summary>
/// Gets or sets the method color. /// Gets or sets the method color.
/// </summary> /// </summary>
public Style Method { get; set; } = new Style(Color.Yellow); public Style Method { get; set; } = Color.Yellow;
/// <summary> /// <summary>
/// Gets or sets the parameter type color. /// Gets or sets the parameter type color.
/// </summary> /// </summary>
public Style ParameterType { get; set; } = new Style(Color.Blue); public Style ParameterType { get; set; } = Color.Blue;
/// <summary> /// <summary>
/// Gets or sets the parameter name color. /// Gets or sets the parameter name color.
/// </summary> /// </summary>
public Style ParameterName { get; set; } = new Style(Color.Silver); public Style ParameterName { get; set; } = Color.Silver;
/// <summary> /// <summary>
/// Gets or sets the parenthesis color. /// Gets or sets the parenthesis color.
/// </summary> /// </summary>
public Style Parenthesis { get; set; } = new Style(Color.Silver); public Style Parenthesis { get; set; } = Color.Silver;
/// <summary> /// <summary>
/// Gets or sets the path color. /// Gets or sets the path color.
@@ -43,15 +43,15 @@ public sealed class ExceptionStyle
/// <summary> /// <summary>
/// Gets or sets the line number color. /// Gets or sets the line number color.
/// </summary> /// </summary>
public Style LineNumber { get; set; } = new Style(Color.Blue); public Style LineNumber { get; set; } = Color.Blue;
/// <summary> /// <summary>
/// Gets or sets the color for dimmed text such as "at" or "in". /// Gets or sets the color for dimmed text such as "at" or "in".
/// </summary> /// </summary>
public Style Dimmed { get; set; } = new Style(Color.Grey); public Style Dimmed { get; set; } = Color.Grey;
/// <summary> /// <summary>
/// Gets or sets the color for non emphasized items. /// Gets or sets the color for non emphasized items.
/// </summary> /// </summary>
public Style NonEmphasized { get; set; } = new Style(Color.Silver); public Style NonEmphasized { get; set; } = Color.Silver;
} }

View File

@@ -49,8 +49,8 @@ public sealed class Grid : JustInTimeRenderable, IExpandable, IAlignable
{ {
_expand = false; _expand = false;
_alignment = null; _alignment = null;
_columns = new ListWithCallback<GridColumn>(() => MarkAsDirty()); _columns = new ListWithCallback<GridColumn>(MarkAsDirty);
_rows = new ListWithCallback<GridRow>(() => MarkAsDirty()); _rows = new ListWithCallback<GridRow>(MarkAsDirty);
} }
/// <summary> /// <summary>

View File

@@ -68,8 +68,6 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
foreach (var (_, first, last, part) in text.SplitLines().Enumerate()) foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
{ {
var current = part;
if (first) if (first)
{ {
var line = _lines.LastOrDefault(); var line = _lines.LastOrDefault();
@@ -79,13 +77,13 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
line = _lines.Last(); line = _lines.Last();
} }
if (string.IsNullOrEmpty(current)) if (string.IsNullOrEmpty(part))
{ {
line.Add(Segment.Empty); line.Add(Segment.Empty);
} }
else else
{ {
foreach (var span in current.SplitWords()) foreach (var span in part.SplitWords())
{ {
line.Add(new Segment(span, style ?? Style.Plain)); line.Add(new Segment(span, style ?? Style.Plain));
} }
@@ -95,13 +93,13 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
{ {
var line = new SegmentLine(); var line = new SegmentLine();
if (string.IsNullOrEmpty(current)) if (string.IsNullOrEmpty(part))
{ {
line.Add(Segment.Empty); line.Add(Segment.Empty);
} }
else else
{ {
foreach (var span in current.SplitWords()) foreach (var span in part.SplitWords())
{ {
line.Add(new Segment(span, style ?? Style.Plain)); line.Add(new Segment(span, style ?? Style.Plain));
} }
@@ -198,13 +196,11 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
var lines = new List<SegmentLine>(); var lines = new List<SegmentLine>();
var line = new SegmentLine(); var line = new SegmentLine();
var newLine = true;
using var iterator = new SegmentLineIterator(_lines); using var iterator = new SegmentLineIterator(_lines);
var queue = new Queue<Segment>(); var queue = new Queue<Segment>();
while (true) while (true)
{ {
var current = (Segment?)null; Segment? current;
if (queue.Count == 0) if (queue.Count == 0)
{ {
if (!iterator.MoveNext()) if (!iterator.MoveNext())
@@ -224,13 +220,12 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
throw new InvalidOperationException("Iterator returned empty segment."); throw new InvalidOperationException("Iterator returned empty segment.");
} }
newLine = false; var newLine = false;
if (current.IsLineBreak) if (current.IsLineBreak)
{ {
lines.Add(line); lines.Add(line);
line = new SegmentLine(); line = new SegmentLine();
newLine = true;
continue; continue;
} }
@@ -246,7 +241,6 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
{ {
lines.Add(line); lines.Add(line);
line = new SegmentLine(); line = new SegmentLine();
newLine = true;
segments.ForEach(s => queue.Enqueue(s)); segments.ForEach(s => queue.Enqueue(s));
continue; continue;
@@ -276,8 +270,6 @@ public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
continue; continue;
} }
newLine = false;
line.Add(current); line.Add(current);
} }

View File

@@ -16,9 +16,9 @@ internal sealed class ProgressBar : Renderable, IHasCulture
public bool IsIndeterminate { get; set; } public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; } public CultureInfo? Culture { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow); public Style CompletedStyle { get; set; } = Color.Yellow;
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green); public Style FinishedStyle { get; set; } = Color.Green;
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey); public Style RemainingStyle { get; set; } = Color.Grey;
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle; public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23); internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
@@ -57,11 +57,6 @@ internal sealed class ProgressBar : Renderable, IHasCulture
barCount = Math.Max(0, barCount); barCount = Math.Max(0, barCount);
} }
if (barCount < 0)
{
yield break;
}
yield return new Segment(new string(bar, barCount), style); yield return new Segment(new string(bar, barCount), style);
if (ShowValue) if (ShowValue)
@@ -99,14 +94,13 @@ internal sealed class ProgressBar : Renderable, IHasCulture
{ {
// For 1-bit and 3-bit colors, fall back to // For 1-bit and 3-bit colors, fall back to
// a simpler versions with only two colors. // a simpler versions with only two colors.
if (options.ColorSystem == ColorSystem.NoColors || if (options.ColorSystem is ColorSystem.NoColors or ColorSystem.Legacy)
options.ColorSystem == ColorSystem.Legacy)
{ {
// First half of the pulse // First half of the pulse
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2); var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
// Second half of the pulse // Second half of the pulse
var legacy = options.ColorSystem == ColorSystem.NoColors || options.ColorSystem == ColorSystem.Legacy; var legacy = options.ColorSystem is ColorSystem.NoColors or ColorSystem.Legacy;
var bar2 = legacy ? " " : bar; var bar2 = legacy ? " " : bar;
segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2))); segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));

View File

@@ -2,8 +2,8 @@ namespace Spectre.Console;
internal static class TableRenderer internal static class TableRenderer
{ {
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver); private static readonly Style _defaultHeadingStyle = Color.Silver;
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey); private static readonly Style _defaultCaptionStyle = Color.Grey;
public static List<Segment> Render(TableRendererContext context, List<int> columnWidths) public static List<Segment> Render(TableRendererContext context, List<int> columnWidths)
{ {

View File

@@ -52,12 +52,12 @@ public sealed class TextPath : IRenderable, IHasJustification
_parts = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); _parts = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
// Rooted Unix path? // Rooted Unix path?
if (path.StartsWith("/")) if (path.StartsWith("/", StringComparison.Ordinal))
{ {
_rooted = true; _rooted = true;
_parts = new[] { "/" }.Concat(_parts).ToArray(); _parts = new[] { "/" }.Concat(_parts).ToArray();
} }
else if (_parts.Length > 0 && _parts[0].EndsWith(":")) else if (_parts.Length > 0 && _parts[0].EndsWith(":", StringComparison.Ordinal))
{ {
// Rooted Windows path // Rooted Windows path
_rooted = true; _rooted = true;

View File

@@ -4,13 +4,20 @@ public static class SpectreAnalyzerVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new() where TAnalyzer : DiagnosticAnalyzer, new()
{ {
public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
=> VerifyCodeFixAsync(source, new[] { expected }, fixedSource); => VerifyCodeFixAsync(source, OutputKind.DynamicallyLinkedLibrary, new[] { expected }, fixedSource);
public static Task VerifyCodeFixAsync(string source, OutputKind outputKind, DiagnosticResult expected, string fixedSource)
=> VerifyCodeFixAsync(source, outputKind, new[] { expected }, fixedSource);
private static Task VerifyCodeFixAsync(string source, IEnumerable<DiagnosticResult> expected, string fixedSource) private static Task VerifyCodeFixAsync(string source, OutputKind outputKind, IEnumerable<DiagnosticResult> expected, string fixedSource)
{ {
var test = new Test var test = new Test
{ {
TestCode = source, TestCode = source,
TestState =
{
OutputKind = outputKind,
},
FixedCode = fixedSource, FixedCode = fixedSource,
}; };

View File

@@ -104,6 +104,74 @@ class TestClass
.ConfigureAwait(false); .ConfigureAwait(false);
} }
[Fact]
public async Task SystemConsole_replaced_with_local_variable_AnsiConsole()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
IAnsiConsole ansiConsole = null;
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
IAnsiConsole ansiConsole = null;
ansiConsole.WriteLine(""Hello, World"");
}
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_not_replaced_with_local_variable_declared_after_the_call()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
Console.WriteLine(""Hello, World"");
IAnsiConsole ansiConsole;
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
AnsiConsole.WriteLine(""Hello, World"");
IAnsiConsole ansiConsole;
}
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact] [Fact]
public async Task SystemConsole_replaced_with_static_field_AnsiConsole() public async Task SystemConsole_replaced_with_static_field_AnsiConsole()
{ {
@@ -139,4 +207,127 @@ class TestClass
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource) .VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
[Fact]
public async Task SystemConsole_replaced_with_AnsiConsole_when_field_is_not_static()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole;
static void TestMethod()
{
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole;
static void TestMethod()
{
AnsiConsole.WriteLine(""Hello, World"");
}
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_replaced_with_AnsiConsole_from_local_function_parameter()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
static void TestMethod()
{
static void LocalFunction(IAnsiConsole ansiConsole) => Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
static void TestMethod()
{
static void LocalFunction(IAnsiConsole ansiConsole) => ansiConsole.WriteLine(""Hello, World"");
}
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 64), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_do_not_use_variable_from_parent_method_in_static_local_function()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
static void TestMethod()
{
IAnsiConsole ansiConsole = null;
static void LocalFunction() => Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
static void TestMethod()
{
IAnsiConsole ansiConsole = null;
static void LocalFunction() => AnsiConsole.WriteLine(""Hello, World"");
}
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 40), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_replaced_with_AnsiConsole_in_top_level_statements()
{
const string Source = @"
using System;
Console.WriteLine(""Hello, World"");
";
const string FixedSource = @"
using System;
using Spectre.Console;
AnsiConsole.WriteLine(""Hello, World"");
";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, OutputKind.ConsoleApplication, _expectedDiagnostic.WithLocation(4, 1), FixedSource)
.ConfigureAwait(false);
}
} }

View File

@@ -1,9 +1,9 @@
namespace Spectre.Console.Tests.Data; namespace Spectre.Console.Tests.Data;
[Description("The horse command.")] [Description("The horse command.")]
public class HorseCommand : AnimalCommand<MammalSettings> public class HorseCommand : AnimalCommand<HorseSettings>
{ {
public override int Execute(CommandContext context, MammalSettings settings) public override int Execute(CommandContext context, HorseSettings settings)
{ {
DumpSettings(context, settings); DumpSettings(context, settings);
return 0; return 0;

View File

@@ -0,0 +1,15 @@
using System.IO;
namespace Spectre.Console.Tests.Data;
public class HorseSettings : MammalSettings
{
[CommandOption("-d|--day")]
public DayOfWeek Day { get; set; }
[CommandOption("--file")]
public FileInfo File { get; set; }
[CommandOption("--directory")]
public DirectoryInfo Directory { get; set; }
}

View File

@@ -9,4 +9,9 @@ public class LionSettings : CatSettings
[CommandOption("-c <CHILDREN>")] [CommandOption("-c <CHILDREN>")]
[Description("The number of children the lion has.")] [Description("The number of children the lion has.")]
public int Children { get; set; } public int Children { get; set; }
[CommandOption("-d <DAY>")]
[Description("The days the lion goes hunting.")]
[DefaultValue(new[] { DayOfWeek.Monday, DayOfWeek.Thursday })]
public DayOfWeek[] HuntDays { get; set; }
} }

View File

@@ -33,3 +33,18 @@ public sealed class RequiredArgumentWithDefaultValueSettings : CommandSettings
[DefaultValue("Hello World")] [DefaultValue("Hello World")]
public string Greeting { get; set; } public string Greeting { get; set; }
} }
public sealed class OptionWithArrayOfEnumDefaultValueSettings : CommandSettings
{
[CommandOption("--days")]
[DefaultValue(new[] { DayOfWeek.Sunday, DayOfWeek.Saturday })]
public DayOfWeek[] Days { get; set; }
}
public sealed class OptionWithArrayOfStringDefaultValueAndTypeConverterSettings : CommandSettings
{
[CommandOption("--numbers")]
[DefaultValue(new[] { "2", "3" })]
[TypeConverter(typeof(StringToIntegerConverter))]
public int[] Numbers { get; set; }
}

View File

@@ -10,8 +10,9 @@ ARGUMENTS:
OPTIONS: OPTIONS:
DEFAULT DEFAULT
-h, --help Prints help information -h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive -a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE> -n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -13,8 +13,9 @@ ARGUMENTS:
OPTIONS: OPTIONS:
DEFAULT DEFAULT
-h, --help Prints help information -h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive -a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE> -n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -10,8 +10,9 @@ ARGUMENTS:
OPTIONS: OPTIONS:
DEFAULT DEFAULT
-h, --help Prints help information -h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive -a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE> -n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -10,11 +10,12 @@ ARGUMENTS:
OPTIONS: OPTIONS:
DEFAULT DEFAULT
-h, --help Prints help information -h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive -a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE> -n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
COMMANDS: COMMANDS:
giraffe <LENGTH> The giraffe command giraffe <LENGTH> The giraffe command

View File

@@ -8,5 +8,7 @@ ARGUMENTS:
<TEETH> The number of teeth the lion has <TEETH> The number of teeth the lion has
OPTIONS: OPTIONS:
-h, --help Prints help information DEFAULT
-c <CHILDREN> The number of children the lion has -h, --help Prints help information
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Model> <Model>
<!--ANIMAL--> <!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings"> <Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
@@ -27,9 +27,11 @@
</Parameters> </Parameters>
</Command> </Command>
<!--HORSE--> <!--HORSE-->
<Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.MammalSettings"> <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters> <Parameters>
<Option Shadowed="true" Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" /> <Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
</Parameters> </Parameters>
</Command> </Command>
</Command> </Command>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Model> <Model>
<!--ANIMAL--> <!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings"> <Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
@@ -23,8 +23,11 @@
</Parameters> </Parameters>
</Command> </Command>
<!--HORSE--> <!--HORSE-->
<Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.MammalSettings"> <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters> <Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" /> <Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters> </Parameters>
</Command> </Command>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Model> <Model>
<!--DEFAULT COMMAND--> <!--DEFAULT COMMAND-->
<Command Name="__default_command" IsBranch="false" IsDefault="true" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings"> <Command Name="__default_command" IsBranch="false" IsDefault="true" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings">
@@ -19,7 +19,7 @@
</Parameters> </Parameters>
</Command> </Command>
<!--HORSE--> <!--HORSE-->
<Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.MammalSettings"> <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters> <Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32"> <Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description> <Description>The number of legs.</Description>
@@ -31,6 +31,9 @@
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean"> <Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description> <Description>Indicates whether or not the animal is alive.</Description>
</Option> </Option>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" /> <Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters> </Parameters>
</Command> </Command>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--MAMMAL-->
<Command Name="mammal" IsBranch="true" Settings="Spectre.Console.Tests.Data.MammalSettings">
<Parameters>
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
</Parameters>
</Command>
</Command>
</Command>
</Model>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--DOG-->
<Command Name="dog" IsBranch="false" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings">
<Parameters>
<Argument Name="AGE" Position="0" Required="true" Kind="scalar" ClrType="System.Int32" />
<Option Short="g" Long="good-boy" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
</Command>
</Model>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--DEFAULT COMMAND-->
<Command Name="__default_command" IsBranch="false" IsDefault="true" ClrType="Spectre.Console.Tests.Data.EmptyCommand" Settings="Spectre.Console.Cli.EmptyCommandSettings" />
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--DOG-->
<Command Name="dog" IsBranch="false" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings">
<Parameters>
<Argument Name="AGE" Position="0" Required="true" Kind="scalar" ClrType="System.Int32" />
<Option Short="g" Long="good-boy" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
</Command>
</Model>

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

@@ -1,5 +1,3 @@
using Spectre.Console.Cli;
namespace Spectre.Console.Tests.Unit.Cli; namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests public sealed partial class CommandAppTests
@@ -41,7 +39,7 @@ public sealed partial class CommandAppTests
configurator.AddCommand<DogCommand>("dog"); configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse"); configurator.AddCommand<HorseCommand>("horse");
configurator.AddCommand<GiraffeCommand>("giraffe") configurator.AddCommand<GiraffeCommand>("giraffe")
.WithExample(new[] { "giraffe", "123" }) .WithExample("giraffe", "123")
.IsHidden(); .IsHidden();
}); });
@@ -64,7 +62,7 @@ public sealed partial class CommandAppTests
configurator.AddCommand<DogCommand>("dog"); configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse"); configurator.AddCommand<HorseCommand>("horse");
configurator.AddCommand<GiraffeCommand>("giraffe") configurator.AddCommand<GiraffeCommand>("giraffe")
.WithExample(new[] { "giraffe", "123" }) .WithExample("giraffe", "123")
.IsHidden(); .IsHidden();
configurator.TrimTrailingPeriods(false); configurator.TrimTrailingPeriods(false);
}); });
@@ -232,8 +230,8 @@ public sealed partial class CommandAppTests
fixture.Configure(configurator => fixture.Configure(configurator =>
{ {
configurator.SetApplicationName("myapp"); configurator.SetApplicationName("myapp");
configurator.AddExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); configurator.AddExample("dog", "--name", "Rufus", "--age", "12", "--good-boy");
configurator.AddExample(new[] { "horse", "--name", "Brutus" }); configurator.AddExample("horse", "--name", "Brutus");
configurator.AddCommand<DogCommand>("dog"); configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse"); configurator.AddCommand<HorseCommand>("horse");
}); });
@@ -255,9 +253,9 @@ public sealed partial class CommandAppTests
{ {
configurator.SetApplicationName("myapp"); configurator.SetApplicationName("myapp");
configurator.AddCommand<DogCommand>("dog") configurator.AddCommand<DogCommand>("dog")
.WithExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy");
configurator.AddCommand<HorseCommand>("horse") configurator.AddCommand<HorseCommand>("horse")
.WithExample(new[] { "horse", "--name", "Brutus" }); .WithExample("horse", "--name", "Brutus");
}); });
// When // When
@@ -280,9 +278,9 @@ public sealed partial class CommandAppTests
{ {
animal.SetDescription("The animal command."); animal.SetDescription("The animal command.");
animal.AddCommand<DogCommand>("dog") animal.AddCommand<DogCommand>("dog")
.WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy");
animal.AddCommand<HorseCommand>("horse") animal.AddCommand<HorseCommand>("horse")
.WithExample(new[] { "animal", "horse", "--name", "Brutus" }); .WithExample("animal", "horse", "--name", "Brutus");
}); });
}); });
@@ -308,9 +306,9 @@ public sealed partial class CommandAppTests
animal.AddExample(new[] { "animal", "--help" }); animal.AddExample(new[] { "animal", "--help" });
animal.AddCommand<DogCommand>("dog") animal.AddCommand<DogCommand>("dog")
.WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy");
animal.AddCommand<HorseCommand>("horse") animal.AddCommand<HorseCommand>("horse")
.WithExample(new[] { "animal", "horse", "--name", "Brutus" }); .WithExample("animal", "horse", "--name", "Brutus");
}); });
}); });
@@ -331,7 +329,7 @@ public sealed partial class CommandAppTests
fixture.Configure(configurator => fixture.Configure(configurator =>
{ {
configurator.SetApplicationName("myapp"); configurator.SetApplicationName("myapp");
configurator.AddExample(new[] { "12", "-c", "3" }); configurator.AddExample("12", "-c", "3");
}); });
// When // When

View File

@@ -3,7 +3,167 @@ namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests public sealed partial class CommandAppTests
{ {
public sealed class Remaining 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] [Fact]
public void Should_Register_Remaining_Parsed_Arguments_With_Context() 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[0].ShouldBe("/c");
result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\""); result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\"");
result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' "); 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

@@ -1,3 +1,5 @@
using System.IO;
namespace Spectre.Console.Tests.Unit.Cli; namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests public sealed partial class CommandAppTests
@@ -29,5 +31,74 @@ public sealed partial class CommandAppTests
cat.Agility.ShouldBe(6); cat.Agility.ShouldBe(6);
}); });
} }
[Fact]
public void Should_Convert_Enum_Ignoring_Case()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddCommand<HorseCommand>("horse");
});
// When
var result = app.Run(new[] { "horse", "--day", "friday" });
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<HorseSettings>().And(horse =>
{
horse.Day.ShouldBe(DayOfWeek.Friday);
});
}
[Fact]
public void Should_List_All_Valid_Enum_Values_On_Conversion_Error()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddCommand<HorseCommand>("horse");
});
// When
var result = app.Run(new[] { "horse", "--day", "heimday" });
// Then
result.ExitCode.ShouldBe(-1);
result.Output.ShouldStartWith("Error");
result.Output.ShouldContain("heimday");
result.Output.ShouldContain(nameof(DayOfWeek.Sunday));
result.Output.ShouldContain(nameof(DayOfWeek.Monday));
result.Output.ShouldContain(nameof(DayOfWeek.Tuesday));
result.Output.ShouldContain(nameof(DayOfWeek.Wednesday));
result.Output.ShouldContain(nameof(DayOfWeek.Thursday));
result.Output.ShouldContain(nameof(DayOfWeek.Friday));
result.Output.ShouldContain(nameof(DayOfWeek.Saturday));
}
[Fact]
public void Should_Convert_FileInfo_And_DirectoryInfo()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddCommand<HorseCommand>("horse");
});
// When
var result = app.Run(new[] { "horse", "--file", "ntp.conf", "--directory", "etc" });
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<HorseSettings>().And(horse =>
{
horse.File.Name.ShouldBe("ntp.conf");
horse.Directory.Name.ShouldBe("etc");
});
}
} }
} }

View File

@@ -130,6 +130,77 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output); 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] [Fact]
[Expectation("Hidden_Command_Options")] [Expectation("Hidden_Command_Options")]
public Task Should_Not_Dump_Hidden_Options_On_A_Command() public Task Should_Not_Dump_Hidden_Options_On_A_Command()

View File

@@ -4,42 +4,6 @@ public sealed partial class CommandAppTests
{ {
[Fact] [Fact]
public void Should_Pass_Case_1() 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 // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@@ -52,8 +16,8 @@ public sealed partial class CommandAppTests
// When // When
var result = app.Run(new[] var result = app.Run(new[]
{ {
"dog", "12", "4", "--good-boy", "dog", "12", "4", "--good-boy",
"--name", "Rufus", "--alive", "--name", "Rufus", "--alive",
}); });
// Then // Then
@@ -69,7 +33,7 @@ public sealed partial class CommandAppTests
} }
[Fact] [Fact]
public void Should_Pass_Case_3() public void Should_Pass_Case_2()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@@ -132,8 +96,8 @@ public sealed partial class CommandAppTests
dog.IsAlive.ShouldBe(false); dog.IsAlive.ShouldBe(false);
dog.Name.ShouldBe("Rufus"); dog.Name.ShouldBe("Rufus");
}); });
} }
[Fact] [Fact]
public void Should_Pass_Case_5() public void Should_Pass_Case_5()
{ {
@@ -165,7 +129,7 @@ public sealed partial class CommandAppTests
dog.IsAlive.ShouldBe(true); dog.IsAlive.ShouldBe(true);
dog.Name.ShouldBe("Rufus"); dog.Name.ShouldBe("Rufus");
}); });
} }
[Fact] [Fact]
public void Should_Pass_Case_6() public void Should_Pass_Case_6()
@@ -197,7 +161,7 @@ public sealed partial class CommandAppTests
} }
[Fact] [Fact]
public void Should_Pass_Case_7() public void Should_Pass_Case_3()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@@ -237,10 +201,10 @@ public sealed partial class CommandAppTests
var result = app.Run(new[] var result = app.Run(new[]
{ {
"dog", "12", "4", "dog", "12", "4",
"--name=\" -Rufus --' ", "--name=\" -Rufus --' ",
"--", "--",
"--order-by", "\"-size\"", "--order-by", "\"-size\"",
"--order-by", " ", "--order-by", " ",
"--order-by", string.Empty, "--order-by", string.Empty,
}); });
@@ -249,8 +213,8 @@ public sealed partial class CommandAppTests
result.Settings.ShouldBeOfType<DogSettings>().And(dog => result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{ {
dog.Name.ShouldBe("\" -Rufus --' "); dog.Name.ShouldBe("\" -Rufus --' ");
}); });
result.Context.Remaining.Parsed.Count.ShouldBe(1); result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("order-by", values: new[] { "\"-size\"", " ", string.Empty }); result.Context.ShouldHaveRemainingArgument("order-by", values: new[] { "\"-size\"", " ", string.Empty });
} }
@@ -280,6 +244,65 @@ public sealed partial class CommandAppTests
}); });
} }
[Fact]
public void Should_Be_Able_To_Use_Branch_Alias()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.AddCommand<HorseCommand>("horse");
}).WithAlias("a");
});
// When
var result = app.Run(new[]
{
"a", "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_Throw_If_Branch_Alias_Conflicts_With_Another_Command()
{
// Given
var app = new CommandApp();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddCommand<DogCommand>("a");
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.AddCommand<HorseCommand>("horse");
}).WithAlias("a");
});
// When
var result = Record.Exception(() => app.Run(new[] { "a", "0", "12" }));
// Then
result.ShouldBeOfType<CommandConfigurationException>().And(ex =>
{
ex.Message.ShouldBe("The alias 'a' for 'animal' conflicts with another command.");
});
}
[Fact] [Fact]
public void Should_Assign_Default_Value_To_Optional_Argument() public void Should_Assign_Default_Value_To_Optional_Argument()
{ {
@@ -373,6 +396,50 @@ public sealed partial class CommandAppTests
}); });
} }
[Fact]
public void Should_Assign_Array_Default_Value_To_Command_Option()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<GenericCommand<OptionWithArrayOfEnumDefaultValueSettings>>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var result = app.Run(Array.Empty<string>());
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<OptionWithArrayOfEnumDefaultValueSettings>().And(settings =>
{
settings.Days.ShouldBe(new[] { DayOfWeek.Sunday, DayOfWeek.Saturday });
});
}
[Fact]
public void Should_Assign_Array_Default_Value_To_Command_Option_Using_Converter_If_Necessary()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<GenericCommand<OptionWithArrayOfStringDefaultValueAndTypeConverterSettings>>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var result = app.Run(Array.Empty<string>());
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<OptionWithArrayOfStringDefaultValueAndTypeConverterSettings>().And(settings =>
{
settings.Numbers.ShouldBe(new[] { 2, 3 });
});
}
[Fact] [Fact]
public void Should_Throw_If_Required_Argument_Have_Default_Value() public void Should_Throw_If_Required_Argument_Have_Default_Value()
{ {
@@ -554,181 +621,181 @@ public sealed partial class CommandAppTests
{ {
dog.IsAlive.ShouldBe(expected); dog.IsAlive.ShouldBe(expected);
}); });
} }
[Fact] [Fact]
public void Should_Set_Short_Option_Before_Argument() public void Should_Set_Short_Option_Before_Argument()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddCommand<DogCommand>("dog"); config.AddCommand<DogCommand>("dog");
}); });
// When // When
var result = app.Run(new[] { "dog", "-a", "-n=Rufus", "4", "12", }); var result = app.Run(new[] { "dog", "-a", "-n=Rufus", "4", "12", });
// Then // Then
result.ExitCode.ShouldBe(0); result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(settings => result.Settings.ShouldBeOfType<DogSettings>().And(settings =>
{ {
settings.IsAlive.ShouldBeTrue(); settings.IsAlive.ShouldBeTrue();
settings.Name.ShouldBe("Rufus"); settings.Name.ShouldBe("Rufus");
settings.Legs.ShouldBe(4); settings.Legs.ShouldBe(4);
settings.Age.ShouldBe(12); settings.Age.ShouldBe(12);
}); });
} }
[Theory] [Theory]
[InlineData("true", true)] [InlineData("true", true)]
[InlineData("True", true)] [InlineData("True", true)]
[InlineData("false", false)] [InlineData("false", false)]
[InlineData("False", false)] [InlineData("False", false)]
public void Should_Set_Short_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected) public void Should_Set_Short_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected)
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddCommand<DogCommand>("dog"); config.AddCommand<DogCommand>("dog");
}); });
// When // When
var result = app.Run(new[] { "dog", "-a", value, "4", "12", }); var result = app.Run(new[] { "dog", "-a", value, "4", "12", });
// Then // Then
result.ExitCode.ShouldBe(0); result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(settings => result.Settings.ShouldBeOfType<DogSettings>().And(settings =>
{ {
settings.IsAlive.ShouldBe(expected); settings.IsAlive.ShouldBe(expected);
settings.Legs.ShouldBe(4); settings.Legs.ShouldBe(4);
settings.Age.ShouldBe(12); settings.Age.ShouldBe(12);
}); });
} }
[Fact] [Fact]
public void Should_Set_Long_Option_Before_Argument() public void Should_Set_Long_Option_Before_Argument()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddCommand<DogCommand>("dog"); config.AddCommand<DogCommand>("dog");
}); });
// When // When
var result = app.Run(new[] { "dog", "--alive", "--name=Rufus", "4", "12" }); var result = app.Run(new[] { "dog", "--alive", "--name=Rufus", "4", "12" });
// Then // Then
result.ExitCode.ShouldBe(0); result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(settings => result.Settings.ShouldBeOfType<DogSettings>().And(settings =>
{ {
settings.IsAlive.ShouldBeTrue(); settings.IsAlive.ShouldBeTrue();
settings.Name.ShouldBe("Rufus"); settings.Name.ShouldBe("Rufus");
settings.Legs.ShouldBe(4); settings.Legs.ShouldBe(4);
settings.Age.ShouldBe(12); settings.Age.ShouldBe(12);
}); });
} }
[Theory] [Theory]
[InlineData("true", true)] [InlineData("true", true)]
[InlineData("True", true)] [InlineData("True", true)]
[InlineData("false", false)] [InlineData("false", false)]
[InlineData("False", false)] [InlineData("False", false)]
public void Should_Set_Long_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected) public void Should_Set_Long_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected)
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddCommand<DogCommand>("dog"); config.AddCommand<DogCommand>("dog");
}); });
// When // When
var result = app.Run(new[] { "dog", "--alive", value, "4", "12", }); var result = app.Run(new[] { "dog", "--alive", value, "4", "12", });
// Then // Then
result.ExitCode.ShouldBe(0); result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(settings => result.Settings.ShouldBeOfType<DogSettings>().And(settings =>
{ {
settings.IsAlive.ShouldBe(expected); settings.IsAlive.ShouldBe(expected);
settings.Legs.ShouldBe(4); settings.Legs.ShouldBe(4);
settings.Age.ShouldBe(12); settings.Age.ShouldBe(12);
}); });
} }
[Theory] [Theory]
// Long options // Long options
[InlineData("dog --alive 4 12 --name Rufus", 4, 12, false, true, "Rufus")] [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: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 --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: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 --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")]
[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 // Short options
[InlineData("dog -a 4 12 --name Rufus", 4, 12, false, true, "Rufus")] [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: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 --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: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 -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")]
[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 // Switch around ordering of the options
[InlineData("dog --good-boy:true --name Rufus --alive:true 4 12", 4, 12, true, true, "Rufus")] [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 --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")] [InlineData("dog --name Rufus --good-boy:true --alive:true 4 12", 4, 12, true, true, "Rufus")]
// Inject the command arguments in between the options // 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 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 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 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 --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 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")] [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) // 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 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 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 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 --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 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")] [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) public void Should_Set_Option_Before_Argument(string arguments, int legs, int age, bool goodBoy, bool isAlive, string name)
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddCommand<DogCommand>("dog"); config.AddCommand<DogCommand>("dog");
}); });
// When // When
var result = app.Run(arguments.Split(' ')); var result = app.Run(arguments.Split(' '));
// Then // Then
result.ExitCode.ShouldBe(0); result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(settings => result.Settings.ShouldBeOfType<DogSettings>().And(settings =>
{ {
settings.Legs.ShouldBe(legs); settings.Legs.ShouldBe(legs);
settings.Age.ShouldBe(age); settings.Age.ShouldBe(age);
settings.GoodBoy.ShouldBe(goodBoy); settings.GoodBoy.ShouldBe(goodBoy);
settings.IsAlive.ShouldBe(isAlive); settings.IsAlive.ShouldBe(isAlive);
settings.Name.ShouldBe(name); settings.Name.ShouldBe(name);
}); });
} }
[Fact] [Fact]
@@ -846,7 +913,7 @@ public sealed partial class CommandAppTests
} }
[Fact] [Fact]
public void Should_Be_Able_To_Set_The_Default_Command() public void Should_Run_The_Default_Command()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@@ -869,6 +936,62 @@ public sealed partial class CommandAppTests
}); });
} }
[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] [Fact]
public void Should_Set_Command_Name_In_Context() public void Should_Set_Command_Name_In_Context()
{ {
@@ -917,6 +1040,66 @@ public sealed partial class CommandAppTests
// Then // Then
result.Context.ShouldNotBeNull(); result.Context.ShouldNotBeNull();
result.Context.Data.ShouldBe(123); result.Context.Data.ShouldBe(123);
}
public sealed class Default_Command
{
[Fact]
public void Should_Be_Able_To_Set_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_Set_The_Default_Command_Description_Data_CommandApp()
{
// Given
var app = new CommandApp();
app.SetDefaultCommand<DogCommand>()
.WithDescription("The default command")
.WithData(new string[] { "foo", "bar" });
// When
// Then
app.GetConfigurator().DefaultCommand.ShouldNotBeNull();
app.GetConfigurator().DefaultCommand.Description.ShouldBe("The default command");
app.GetConfigurator().DefaultCommand.Data.ShouldBe(new string[] { "foo", "bar" });
}
[Fact]
public void Should_Set_The_Default_Command_Description_Data_CommandAppOfT()
{
// Given
var app = new CommandApp<DogCommand>()
.WithDescription("The default command")
.WithData(new string[] { "foo", "bar" });
// When
// Then
app.GetConfigurator().DefaultCommand.ShouldNotBeNull();
app.GetConfigurator().DefaultCommand.Description.ShouldBe("The default command");
app.GetConfigurator().DefaultCommand.Data.ShouldBe(new string[] { "foo", "bar" });
}
} }
public sealed class Delegate_Commands public sealed class Delegate_Commands
@@ -930,7 +1113,7 @@ public sealed partial class CommandAppTests
var app = new CommandApp(); var app = new CommandApp();
app.Configure(config => app.Configure(config =>
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddDelegate<DogSettings>( config.AddDelegate<DogSettings>(
"foo", (context, settings) => "foo", (context, settings) =>
@@ -986,67 +1169,4 @@ public sealed partial class CommandAppTests
data.ShouldBe(2); 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");
}
}
} }

View File

@@ -247,42 +247,42 @@ public sealed class TextPromptTests
// Then // Then
return Verifier.Verify(console.Output); return Verifier.Verify(console.Output);
} }
[Fact] [Fact]
[Expectation("SecretDefaultValueCustomMask")] [Expectation("SecretDefaultValueCustomMask")]
public Task Should_Choose_Custom_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Custom() public Task Should_Choose_Custom_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Custom()
{ {
// Given // Given
var console = new TestConsole(); var console = new TestConsole();
console.Input.PushKey(ConsoleKey.Enter); console.Input.PushKey(ConsoleKey.Enter);
// When // When
console.Prompt( console.Prompt(
new TextPrompt<string>("Favorite fruit?") new TextPrompt<string>("Favorite fruit?")
.Secret('-') .Secret('-')
.DefaultValue("Banana")); .DefaultValue("Banana"));
// Then // Then
return Verifier.Verify(console.Output); return Verifier.Verify(console.Output);
} }
[Fact] [Fact]
[Expectation("SecretDefaultValueNullMask")] [Expectation("SecretDefaultValueNullMask")]
public Task Should_Choose_Empty_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Null() public Task Should_Choose_Empty_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Null()
{ {
// Given // Given
var console = new TestConsole(); var console = new TestConsole();
console.Input.PushKey(ConsoleKey.Enter); console.Input.PushKey(ConsoleKey.Enter);
// When // When
console.Prompt( console.Prompt(
new TextPrompt<string>("Favorite fruit?") new TextPrompt<string>("Favorite fruit?")
.Secret(null) .Secret(null)
.DefaultValue("Banana")); .DefaultValue("Banana"));
// Then // Then
return Verifier.Verify(console.Output); return Verifier.Verify(console.Output);
} }
[Fact] [Fact]
@@ -337,7 +337,7 @@ public sealed class TextPromptTests
var prompt = new TextPrompt<string>("Enter Value:") var prompt = new TextPrompt<string>("Enter Value:")
.ShowDefaultValue() .ShowDefaultValue()
.DefaultValue("default") .DefaultValue("default")
.DefaultValueStyle(new Style(foreground: Color.Red)); .DefaultValueStyle(Color.Red);
// When // When
console.Prompt(prompt); console.Prompt(prompt);
@@ -384,7 +384,7 @@ public sealed class TextPromptTests
.ShowChoices() .ShowChoices()
.AddChoice("Choice 1") .AddChoice("Choice 1")
.AddChoice("Choice 2") .AddChoice("Choice 2")
.ChoicesStyle(new Style(foreground: Color.Red)); .ChoicesStyle(Color.Red);
// When // When
console.Prompt(prompt); console.Prompt(prompt);

View File

@@ -1,7 +1,20 @@
namespace Spectre.Console.Tests.Unit; namespace Spectre.Console.Tests.Unit;
public sealed class StyleTests public sealed class StyleTests
{ {
[Fact]
public void Should_Convert_From_Color_As_Expected()
{
// Given
Style style;
// When
style = Color.Red;
// Then
style.Foreground.ShouldBe(Color.Red);
}
[Fact] [Fact]
public void Should_Combine_Two_Styles_As_Expected() public void Should_Combine_Two_Styles_As_Expected()
{ {