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")
.WithAlias("hola")
.WithDescription("Say hello")
.WithExample(new []{"hello", "Phil"})
.WithExample(new []{"hello", "Phil", "--count", "4"});
.WithExample("hello", "Phil")
.WithExample("hello", "Phil", "--count", "4");
});
```

View File

@@ -31,7 +31,7 @@ var fruits = AnsiConsole.Prompt(
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
"Cherry", "Cloudberry", "Coconut",
}));
// Write the selected fruits to the terminal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,11 +16,15 @@ public class FavorInstanceAnsiConsoleOverStaticAnalyzer : SpectreAnalyzer
/// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{
var ansiConsoleType = compilationStartContext.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
if (ansiConsoleType == null)
{
return;
}
compilationStartContext.RegisterOperationAction(
context =>
{
var ansiConsoleType = context.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
// 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;
var invocationOperation = (IInvocationOperation)context.Operation;

View File

@@ -17,6 +17,16 @@ public class NoConcurrentLiveRenderablesAnalyzer : SpectreAnalyzer
/// <inheritdoc />
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(
context =>
{
@@ -29,27 +39,21 @@ public class NoConcurrentLiveRenderablesAnalyzer : SpectreAnalyzer
return;
}
var liveTypes = Constants.LiveRenderables
.Select(i => context.Compilation.GetTypeByMetadataName(i))
.ToImmutableArray();
if (liveTypes.All(i => !SymbolEqualityComparer.Default.Equals(i, methodSymbol.ContainingType)))
{
return;
}
#pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
var model = context.Compilation.GetSemanticModel(context.Operation.Syntax.SyntaxTree);
#pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
var model = context.Operation.SemanticModel!;
var parentInvocations = invocationOperation
.Syntax.Ancestors()
.OfType<InvocationExpressionSyntax>()
.Select(i => model.GetOperation(i))
.Select(i => model.GetOperation(i, context.CancellationToken))
.OfType<IInvocationOperation>()
.ToList();
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;
}

View File

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

View File

@@ -18,6 +18,13 @@ public class UseSpectreInsteadOfSystemConsoleAnalyzer : SpectreAnalyzer
/// <inheritdoc />
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(
context =>
{
@@ -31,8 +38,6 @@ public class UseSpectreInsteadOfSystemConsoleAnalyzer : SpectreAnalyzer
return;
}
var systemConsoleType = context.Compilation.GetTypeByMetadataName("System.Console");
if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.ContainingType, systemConsoleType))
{
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;
@@ -32,82 +33,170 @@ public class SwitchToAnsiConsoleAction : CodeAction
/// <inheritdoc />
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;
}
var root = (CompilationUnitSyntax)await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
// If there is an ansiConsole passed into the method then we'll use it.
// If there is an IAnsiConsole passed into the method then we'll use it.
// otherwise we'll check for a field level instance.
// if neither of those exist we'll fall back to the static param.
var ansiConsoleParameterDeclaration = GetAnsiConsoleParameterDeclaration();
var ansiConsoleFieldIdentifier = GetAnsiConsoleFieldDeclaration();
var ansiConsoleIdentifier = ansiConsoleParameterDeclaration ??
ansiConsoleFieldIdentifier ??
Constants.StaticInstance;
var spectreConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
var iansiConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.IAnsiConsole");
// Replace the System.Console call with a call to the identifier above.
var newRoot = root.ReplaceNode(
_originalInvocation,
GetImportedSpectreCall(originalCaller, ansiConsoleIdentifier));
// If we are calling the static instance and Spectre isn't imported yet we should do so.
if (ansiConsoleIdentifier == Constants.StaticInstance && root.Usings.ToList().All(i => i.Name.ToString() != Constants.SpectreConsole))
ISymbol? accessibleConsoleSymbol = spectreConsoleSymbol;
if (iansiConsoleSymbol != null)
{
newRoot = newRoot.AddUsings(Syntax.SpectreUsing);
var isInStaticContext = IsInStaticContext(operation, cancellationToken, out var parentStaticMemberStartPosition);
foreach (var symbol in editor.SemanticModel.LookupSymbols(operation.Syntax.GetLocation().SourceSpan.Start))
{
// LookupSymbols check the accessibility of the symbol, but it can
// suggest instance members when the current context is static.
var symbolType = symbol switch
{
IParameterSymbol parameter => parameter.Type,
IFieldSymbol field when !isInStaticContext || field.IsStatic => field.Type,
IPropertySymbol { GetMethod: not null } property when !isInStaticContext || property.IsStatic => property.Type,
ILocalSymbol local => local.Type,
_ => null,
};
// 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
// should not be accessible in a static local function.
//
// void Sample()
// {
// int local = 0;
// static void LocalFunction() => local; <-- local is invalid here but LookupSymbols suggests it
// }
//
// 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)
{
var localPosition = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken).GetLocation().SourceSpan.Start;
// The local is not part of the source tree
if (localPosition == null)
{
break;
}
return _document.WithSyntaxRoot(newRoot);
// The local is declared after the current expression
if (localPosition > _originalInvocation.Span.Start)
{
break;
}
private string? GetAnsiConsoleParameterDeclaration()
// The local is declared outside the static local function
if (isInStaticContext && localPosition < parentStaticMemberStartPosition)
{
return _originalInvocation
.Ancestors().OfType<MethodDeclarationSyntax>()
.First()
.ParameterList.Parameters
.FirstOrDefault(i => i.Type?.NormalizeWhitespace()?.ToString() == "IAnsiConsole")
?.Identifier.Text;
break;
}
}
private string? GetAnsiConsoleFieldDeclaration()
if (IsOrImplementSymbol(symbolType, iansiConsoleSymbol))
{
// let's look to see if our call is in a static method.
// if so we'll only want to look for static IAnsiConsoles
// and vice-versa if we aren't.
var isStatic = _originalInvocation
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.First()
.Modifiers.Any(i => i.IsKind(SyntaxKind.StaticKeyword));
return _originalInvocation
.Ancestors().OfType<ClassDeclarationSyntax>()
.First()
.Members
.OfType<FieldDeclarationSyntax>()
.FirstOrDefault(i =>
i.Declaration.Type.NormalizeWhitespace().ToString() == "IAnsiConsole" &&
(!isStatic ^ i.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword))))
?.Declaration.Variables.First().Identifier.Text;
accessibleConsoleSymbol = symbol;
break;
}
}
}
private ExpressionSyntax GetImportedSpectreCall(string originalCaller, string ansiConsoleIdentifier)
if (accessibleConsoleSymbol == null)
{
return ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(ansiConsoleIdentifier),
IdentifierName(originalCaller)))
.WithArgumentList(_originalInvocation.ArgumentList)
.WithTrailingTrivia(_originalInvocation.GetTrailingTrivia())
.WithLeadingTrivia(_originalInvocation.GetLeadingTrivia()))
.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);
if (root != null)
{
var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>();
var methodDeclaration = root.FindNode(context.Span, getInnermostNodeForTie: true).FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (methodDeclaration != null)
{
context.RegisterCodeFix(

View File

@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli;
/// <summary>
@@ -39,10 +41,11 @@ public sealed class CommandApp : ICommandApp
/// Sets the default command.
/// </summary>
/// <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
{
GetConfigurator().SetDefaultCommand<TCommand>();
return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand<TCommand>());
}
/// <summary>

View File

@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli;
/// <summary>
@@ -8,6 +10,7 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
where TDefaultCommand : class, ICommand
{
private readonly CommandApp _app;
private readonly DefaultCommandConfigurator _defaultCommandConfigurator;
/// <summary>
/// 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)
{
_app = new CommandApp(registrar);
_app.GetConfigurator().SetDefaultCommand<TDefaultCommand>();
_defaultCommandConfigurator = _app.SetDefaultCommand<TDefaultCommand>();
}
/// <summary>
@@ -47,4 +50,31 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
{
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}'.");
}
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)
{
return new CommandRuntimeException(result.Message ?? "Unknown validation error.");

View File

@@ -181,7 +181,8 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="name">The name of the command branch.</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,
string name,
Action<IConfigurator<CommandSettings>> action)
@@ -191,7 +192,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
configurator.AddBranch(name, action);
return configurator.AddBranch(name, action);
}
/// <summary>
@@ -201,7 +202,8 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="name">The name of the command branch.</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,
string name,
Action<IConfigurator<TSettings>> action)
@@ -212,7 +214,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
configurator.AddBranch(name, action);
return configurator.AddBranch(name, action);
}
/// <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

@@ -51,6 +51,14 @@ public interface ICommandAppSettings
/// </summary>
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>
/// Gets or sets a value indicating whether or not exceptions should be propagated.
/// <para>Setting this to <c>true</c> will disable default Exception handling and

View File

@@ -10,7 +10,7 @@ public interface ICommandConfigurator
/// </summary>
/// <param name="args">The example arguments.</param>
/// <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>
/// 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.
/// </summary>
/// <param name="args">The example arguments.</param>
void AddExample(string[] args);
void AddExample(params string[] args);
/// <summary>
/// Adds a command.
@@ -41,6 +41,7 @@ public interface IConfigurator
/// <typeparam name="TSettings">The command setting type.</typeparam>
/// <param name="name">The name of the command branch.</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;
}

View File

@@ -19,6 +19,18 @@ public interface IConfigurator<in TSettings>
/// <param name="args">The example arguments.</param>
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>
/// Marks the branch as hidden.
/// 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>
/// <param name="name">The name of the command branch.</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;
}

View File

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

View File

@@ -78,14 +78,32 @@ internal static class CommandValueResolver
}
else
{
var converter = GetConverter(lookup, binder, resolver, mapped.Parameter);
var (converter, stringConstructor) = GetConverter(lookup, binder, resolver, mapped.Parameter);
if (converter == null)
{
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.
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)
{
var converter = GetConverter(lookup, binder, resolver, parameter);
var (converter, _) = GetConverter(lookup, binder, resolver, parameter);
if (converter != null)
{
result = converter.ConvertFrom(result);
result = result is Array array ? ConvertArray(array, converter) : converter.ConvertFrom(result);
}
}
return result;
}
[SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")]
private static TypeConverter? GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter)
private static Array ConvertArray(Array sourceArray, TypeConverter converter)
{
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.ParameterType.IsArray)
@@ -136,12 +180,12 @@ internal static class CommandValueResolver
throw new InvalidOperationException("Could not get element type");
}
return TypeDescriptor.GetConverter(elementType);
return (TypeDescriptor.GetConverter(elementType), GetStringConstructor(elementType));
}
if (parameter.IsFlagValue())
{
// Is the optional value instanciated?
// Is the optional value instantiated?
var value = lookup.GetValue(parameter) as IFlagValue;
if (value == null)
{
@@ -151,18 +195,18 @@ internal static class CommandValueResolver
value = lookup.GetValue(parameter) as IFlagValue;
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 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);
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.
var parser = new CommandTreeParser(model, configuration.Settings);
var parsedResult = parser.Parse(args);
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
// Currently the root?
if (parsedResult.Tree == null)
if (parsedResult?.Tree == null)
{
// Display help.
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues));
@@ -75,6 +73,7 @@ internal sealed class CommandExecutor
}
// Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
_registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);
// Create the resolver and the context.
@@ -87,6 +86,34 @@ internal sealed class CommandExecutor
}
}
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)
{
return

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

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

View File

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

View File

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

View File

@@ -22,6 +22,14 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
_command.Examples.Add(args);
}
public void SetDefaultCommand<TDefaultCommand>()
where TDefaultCommand : class, ICommandLimiter<TSettings>
{
var defaultCommand = ConfiguredCommand.FromType<TDefaultCommand>(
CliConstants.DefaultCommandName, isDefaultCommand: true);
_command.Children.Add(defaultCommand);
}
public void HideBranch()
{
@@ -30,7 +38,7 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
public ICommandConfigurator AddCommand<TCommand>(string name)
where TCommand : class, ICommandLimiter<TSettings>
var command = ConfiguredCommand.FromType<TCommand>(name);
{
var command = ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false);
var configurator = new CommandConfigurator(command);
@@ -47,12 +55,13 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
_command.Children.Add(command);
return new CommandConfigurator(command);
}
public void AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
where TDerivedSettings : TSettings
{
var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
_command.Children.Add(command);
action(new Configurator<TDerivedSettings>(command, _registrar));
var added = _command.Children.AddAndReturn(command);
return new BranchConfigurator(added);
}
@@ -73,7 +82,7 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
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);
@@ -85,6 +94,7 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
throw new CommandConfigurationException("Could not create configurator by reflection.");
}
_command.Children.Add(command);
action(configurator);
var added = _command.Children.AddAndReturn(command);
return new BranchConfigurator(added);
}

View File

@@ -29,6 +29,9 @@ internal sealed class ConfiguredCommand
Delegate = @delegate;
IsDefaultCommand = isDefaultCommand;
// Default commands are always created as hidden.
IsHidden = IsDefaultCommand;
Children = new List<ConfiguredCommand>();
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>();
model.Commands.ForEach(c => stack.Push(c));
if (model.DefaultCommand != null)
{
stack.Push(model.DefaultCommand);
}
while (stack.Count > 0)
{

View File

@@ -348,7 +348,17 @@ internal static class HelpWriter
var columns = new List<string> { GetOptionParts(option) };
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('.') ?? " ");

View File

@@ -10,7 +10,6 @@ internal sealed class CommandInfo : ICommandContainer
public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, int>? Delegate { get; }
public bool IsDefaultCommand { get; }
public bool IsHidden { get; }
public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; }
public IList<CommandParameter> Parameters { get; }
@@ -19,6 +18,10 @@ internal sealed class CommandInfo : ICommandContainer
public bool IsBranch => CommandType == null && Delegate == null;
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)
{
Parent = parent;

View File

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

View File

@@ -25,18 +25,19 @@ internal static class CommandModelBuilder
result.Add(Build(null, command));
}
var defaultCommand = default(CommandInfo);
if (configuration.DefaultCommand != null)
{
// Add the examples from the configuration to the default command.
configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// 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.
var model = new CommandModel(configuration.Settings, defaultCommand, result, configuration.Examples);
var model = new CommandModel(configuration.Settings, result, configuration.Examples);
CommandModelValidator.Validate(model, configuration.Settings);
return model;

View File

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

View File

@@ -9,4 +9,12 @@ internal interface ICommandContainer
/// Gets all commands in the container.
/// </summary>
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,3 +1,5 @@
using static Spectre.Console.Cli.CommandTreeTokenizer;
namespace Spectre.Console.Cli;
internal class CommandTreeParser
@@ -5,6 +7,7 @@ internal class CommandTreeParser
private readonly CommandModel _configuration;
private readonly ParsingMode _parsingMode;
private readonly CommandOptionAttribute _help;
private readonly bool _convertFlagsToRemainingArguments;
public CaseSensitivity CaseSensitivity { get; }
@@ -14,25 +17,26 @@ internal class CommandTreeParser
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)
{
throw new ArgumentNullException(nameof(settings));
}
_configuration = configuration;
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_parsingMode = parsingMode ?? _configuration.ParsingMode;
_help = new CommandOptionAttribute("-h|--help");
_convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false;
CaseSensitivity = settings.CaseSensitivity;
CaseSensitivity = caseSensitivity;
}
public CommandTreeParserResult Parse(IEnumerable<string> args)
{
var context = new CommandTreeParserContext(args, _parsingMode);
var parserContext = new CommandTreeParserContext(args, _parsingMode);
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
var tokenizerResult = CommandTreeTokenizer.Tokenize(context.Arguments);
return Parse(parserContext, tokenizerResult);
}
public CommandTreeParserResult Parse(CommandTreeParserContext context, CommandTreeTokenizerResult tokenizerResult)
{
var tokens = tokenizerResult.Tokens;
var rawRemaining = tokenizerResult.Remaining;
@@ -255,9 +259,7 @@ internal class CommandTreeParser
var option = node.FindOption(token.Value, isLongOption, CaseSensitivity);
if (option != null)
{
node.Mapped.Add(new MappedCommandParameter(
option, ParseOptionValue(context, stream, token, node, option)));
ParseOptionValue(context, stream, token, node, option);
return;
}
@@ -271,7 +273,7 @@ internal class CommandTreeParser
if (context.State == State.Remaining)
{
ParseOptionValue(context, stream, token, node, null);
ParseOptionValue(context, stream, token, node);
return;
}
@@ -281,17 +283,19 @@ internal class CommandTreeParser
}
else
{
ParseOptionValue(context, stream, token, node, null);
ParseOptionValue(context, stream, token, node);
}
}
private string? ParseOptionValue(
private void ParseOptionValue(
CommandTreeParserContext context,
CommandTreeTokenStream stream,
CommandTreeToken token,
CommandTree current,
CommandParameter? parameter)
CommandParameter? parameter = null)
{
bool addToMappedCommandParameters = parameter != null;
var value = default(string);
// Parse the value of the token (if any).
@@ -325,9 +329,23 @@ internal class CommandTreeParser
else
{
// Flags cannot be assigned a value.
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
{
value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
@@ -358,7 +376,8 @@ internal class CommandTreeParser
}
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);
}
@@ -379,11 +398,13 @@ internal class CommandTreeParser
{
if (parameter.IsFlagValue())
{
return null;
value = null;
}
else
{
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
}
}
else
{
// This should not happen at all. If it does, it's because we've added a new
@@ -394,6 +415,9 @@ internal class CommandTreeParser
}
}
return value;
if (parameter != null && addToMappedCommandParameters)
{
current.Mapped.Add(new MappedCommandParameter(parameter, value));
}
}
}

View File

@@ -29,8 +29,6 @@ internal class CommandTreeParserContext
}
public void AddRemainingArgument(string key, string? value)
{
if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed)
{
if (!_remaining.ContainsKey(key))
{
@@ -39,7 +37,6 @@ internal class CommandTreeParserContext
_remaining[key].Add(value);
}
}
[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")]
public ILookup<string, string?> GetRemainingArguments()

View File

@@ -6,6 +6,7 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
private int _position;
public int Count => _tokens.Count;
public int Position => _position;
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="settings">The command setting type.</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
{
BracesStyle = BracesStyle ?? new Style(Color.Grey),
BracketsStyle = BracketsStyle ?? new Style(Color.Grey),
MemberStyle = MemberStyle ?? new Style(Color.Blue),
ColonStyle = ColonStyle ?? new Style(Color.Yellow),
CommaStyle = CommaStyle ?? new Style(Color.Grey),
StringStyle = StringStyle ?? new Style(Color.Red),
NumberStyle = NumberStyle ?? new Style(Color.Green),
BooleanStyle = BooleanStyle ?? new Style(Color.Green),
NullStyle = NullStyle ?? new Style(Color.Grey),
BracesStyle = BracesStyle ?? Color.Grey,
BracketsStyle = BracketsStyle ?? Color.Grey,
MemberStyle = MemberStyle ?? Color.Blue,
ColonStyle = ColonStyle ?? Color.Yellow,
CommaStyle = CommaStyle ?? Color.Grey,
StringStyle = StringStyle ?? Color.Red,
NumberStyle = NumberStyle ?? Color.Green,
BooleanStyle = BooleanStyle ?? Color.Green,
NullStyle = NullStyle ?? Color.Grey,
});
_syntax.Accept(JsonBuilder.Shared, context);

View File

@@ -25,11 +25,25 @@ public sealed class CommandAppTester
/// <summary>
/// Sets the default command.
/// </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>
public void SetDefaultCommand<T>()
public void SetDefaultCommand<T>(string? description = null, object? data = null)
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,15 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
/// </summary>
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>
/// Initializes a new instance of the <see cref="ConfirmationPrompt"/> class.
/// </summary>
@@ -57,7 +66,9 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
/// <inheritdoc/>
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)
.ValidationErrorMessage(InvalidChoiceMessage)
.ShowChoices(ShowChoices)
@@ -67,6 +78,7 @@ public sealed class ConfirmationPrompt : IPrompt<bool>
.AddChoice(No);
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)
{
var list = new List<IRenderable>();
var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue);
var highlightStyle = HighlightStyle ?? Color.Blue;
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)
{
var list = new List<IRenderable>();
var disabledStyle = DisabledStyle ?? new Style(foreground: Color.Grey);
var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue);
var disabledStyle = DisabledStyle ?? Color.Grey;
var highlightStyle = HighlightStyle ?? Color.Blue;
if (Title != null)
{

View File

@@ -132,6 +132,15 @@ public sealed class Style : IEquatable<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>
/// Converts the string representation of a style to its <see cref="Style"/> equivalent.
/// </summary>

View File

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

View File

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

View File

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

View File

@@ -16,9 +16,9 @@ internal sealed class ProgressBar : Renderable, IHasCulture
public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
public Style CompletedStyle { get; set; } = Color.Yellow;
public Style FinishedStyle { get; set; } = Color.Green;
public Style RemainingStyle { get; set; } = Color.Grey;
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
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);
}
if (barCount < 0)
{
yield break;
}
yield return new Segment(new string(bar, barCount), style);
if (ShowValue)
@@ -99,14 +94,13 @@ internal sealed class ProgressBar : Renderable, IHasCulture
{
// For 1-bit and 3-bit colors, fall back to
// a simpler versions with only two colors.
if (options.ColorSystem == ColorSystem.NoColors ||
options.ColorSystem == ColorSystem.Legacy)
if (options.ColorSystem is ColorSystem.NoColors or ColorSystem.Legacy)
{
// First half of the pulse
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
// 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;
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
{
private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
private static readonly Style _defaultHeadingStyle = Color.Silver;
private static readonly Style _defaultCaptionStyle = Color.Grey;
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);
// Rooted Unix path?
if (path.StartsWith("/"))
if (path.StartsWith("/", StringComparison.Ordinal))
{
_rooted = true;
_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 = true;

View File

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

View File

@@ -104,6 +104,74 @@ class TestClass
.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]
public async Task SystemConsole_replaced_with_static_field_AnsiConsole()
{
@@ -139,4 +207,127 @@ class TestClass
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.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;
[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);
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>")]
[Description("The number of children the lion has.")]
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")]
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

@@ -15,3 +15,4 @@ OPTIONS:
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -18,3 +18,4 @@ OPTIONS:
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -15,3 +15,4 @@ OPTIONS:
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@@ -15,6 +15,7 @@ OPTIONS:
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
COMMANDS:
giraffe <LENGTH> The giraffe command

View File

@@ -8,5 +8,7 @@ ARGUMENTS:
<TEETH> The number of teeth the lion has
OPTIONS:
DEFAULT
-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>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
@@ -27,9 +27,11 @@
</Parameters>
</Command>
<!--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>
<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>
</Command>
</Command>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
@@ -23,8 +23,11 @@
</Parameters>
</Command>
<!--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>
<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>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--DEFAULT COMMAND-->
<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>
</Command>
<!--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>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<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">
<Description>Indicates whether or not the animal is alive.</Description>
</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" />
</Parameters>
</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;
public sealed partial class CommandAppTests
@@ -41,7 +39,7 @@ public sealed partial class CommandAppTests
configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse");
configurator.AddCommand<GiraffeCommand>("giraffe")
.WithExample(new[] { "giraffe", "123" })
.WithExample("giraffe", "123")
.IsHidden();
});
@@ -64,7 +62,7 @@ public sealed partial class CommandAppTests
configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse");
configurator.AddCommand<GiraffeCommand>("giraffe")
.WithExample(new[] { "giraffe", "123" })
.WithExample("giraffe", "123")
.IsHidden();
configurator.TrimTrailingPeriods(false);
});
@@ -232,8 +230,8 @@ public sealed partial class CommandAppTests
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" });
configurator.AddExample(new[] { "horse", "--name", "Brutus" });
configurator.AddExample("dog", "--name", "Rufus", "--age", "12", "--good-boy");
configurator.AddExample("horse", "--name", "Brutus");
configurator.AddCommand<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse");
});
@@ -255,9 +253,9 @@ public sealed partial class CommandAppTests
{
configurator.SetApplicationName("myapp");
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")
.WithExample(new[] { "horse", "--name", "Brutus" });
.WithExample("horse", "--name", "Brutus");
});
// When
@@ -280,9 +278,9 @@ public sealed partial class CommandAppTests
{
animal.SetDescription("The animal command.");
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")
.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.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")
.WithExample(new[] { "animal", "horse", "--name", "Brutus" });
.WithExample("animal", "horse", "--name", "Brutus");
});
});
@@ -331,7 +329,7 @@ public sealed partial class CommandAppTests
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample(new[] { "12", "-c", "3" });
configurator.AddExample("12", "-c", "3");
});
// When

View File

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

View File

@@ -1,3 +1,5 @@
using System.IO;
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
@@ -29,5 +31,74 @@ public sealed partial class CommandAppTests
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);
}
[Fact]
[Expectation("Test_7")]
public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Branch_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<HorseCommand>();
});
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Test_8")]
public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Command_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Test_9")]
public Task Should_Dump_Correct_Model_For_Model_With_Default_Command_Single_Branch_Single_Command_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<EmptyCommand>();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Hidden_Command_Options")]
public Task Should_Not_Dump_Hidden_Options_On_A_Command()

View File

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

View File

@@ -337,7 +337,7 @@ public sealed class TextPromptTests
var prompt = new TextPrompt<string>("Enter Value:")
.ShowDefaultValue()
.DefaultValue("default")
.DefaultValueStyle(new Style(foreground: Color.Red));
.DefaultValueStyle(Color.Red);
// When
console.Prompt(prompt);
@@ -384,7 +384,7 @@ public sealed class TextPromptTests
.ShowChoices()
.AddChoice("Choice 1")
.AddChoice("Choice 2")
.ChoicesStyle(new Style(foreground: Color.Red));
.ChoicesStyle(Color.Red);
// When
console.Prompt(prompt);

View File

@@ -2,6 +2,19 @@ namespace Spectre.Console.Tests.Unit;
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]
public void Should_Combine_Two_Styles_As_Expected()
{