diff --git a/docs/input/appendix/index.cshtml b/docs/input/appendix/index.cshtml
index 61dcd86..0485f88 100644
--- a/docs/input/appendix/index.cshtml
+++ b/docs/input/appendix/index.cshtml
@@ -1,5 +1,5 @@
Title: Appendix
-Order: 10
+Order: 100
---
Sections
diff --git a/docs/input/cli/index.cshtml b/docs/input/cli/index.cshtml
new file mode 100644
index 0000000..4f61d5f
--- /dev/null
+++ b/docs/input/cli/index.cshtml
@@ -0,0 +1,12 @@
+Title: CLI
+Order: 10
+---
+
+Sections
+
+
+@foreach (IDocument child in OutputPages.GetChildrenOf(Document))
+{
+ - @Html.DocumentLink(child)
+}
+
\ No newline at end of file
diff --git a/docs/input/cli/introduction.md b/docs/input/cli/introduction.md
new file mode 100644
index 0000000..a38f04d
--- /dev/null
+++ b/docs/input/cli/introduction.md
@@ -0,0 +1,117 @@
+Title: Introduction
+Order: 1
+---
+
+`Spectre.Console.Cli` is a modern library for parsing command line arguments. While it's extremely
+opinionated in what it does, it tries to follow established industry conventions, and draws
+its inspiration from applications you use everyday.
+
+# How does it work?
+
+The underlying philosophy behind `Spectre.Console.Cli` is to rely on the .NET type system to
+declare the commands, but tie everything together via composition.
+
+Imagine the following command structure:
+
+* dotnet *(executable)*
+ * add `[PROJECT]`
+ * package `` --version ``
+ * reference ``
+
+For this I would like to implement the commands (the different levels in the tree that
+executes something) separately from the settings (the options, flags and arguments),
+which I want to be able to inherit from each other.
+
+## Specify the settings
+
+We start by creating some settings that represents the options, flags and arguments
+that we want to act upon.
+
+```csharp
+public class AddSettings : CommandSettings
+{
+ [CommandArgument(0, "[PROJECT]")]
+ public string Project { get; set; }
+}
+
+public class AddPackageSettings : AddSettings
+{
+ [CommandArgument(0, "")]
+ public string PackageName { get; set; }
+
+ [CommandOption("-v|--version ")]
+ public string Version { get; set; }
+}
+
+public class AddReferenceSettings : AddSettings
+{
+ [CommandArgument(0, "")]
+ public string ProjectReference { get; set; }
+}
+```
+
+## Specify the commands
+
+Now it's time to specify the commands that act on the settings we created
+in the previous step.
+
+```csharp
+public class AddPackageCommand : Command
+{
+ public override int Execute(AddPackageSettings settings, ILookup remaining)
+ {
+ // Omitted
+ }
+}
+
+public class AddReferenceCommand : Command
+{
+ public override int Execute(AddReferenceSettings settings, ILookup remaining)
+ {
+ // Omitted
+ }
+}
+```
+
+## Let's tie it together
+
+Now when we have our commands and settings implemented, we can compose a command tree
+that tells the parser how to interpret user input.
+
+```csharp
+using Spectre.Console.Cli;
+
+namespace MyApp
+{
+ public static class Program
+ {
+ public static int Main(string[] args)
+ {
+ var app = new CommandApp();
+
+ app.Configure(config =>
+ {
+ config.AddBranch("add", add =>
+ {
+ add.AddCommand("package");
+ add.AddCommand("reference");
+ });
+ });
+
+ return app.Run(args);
+ }
+ }
+}
+```
+
+# So why this way?
+
+Now you might wonder, why do things like this? Well, for starters the different parts
+of the application are separated, while still having the option to share things like options,
+flags and arguments between them.
+
+This make the resulting code very clean and easy to navigate, not to mention to unit test.
+And most importantly at all, the type system guides me to do the right thing. I can't configure
+commands in non-compatible ways, and if I want to add a new top-level `add-package` command
+(or move the command completely), it's just a single line to change. This makes it easy to
+experiment and makes the CLI experience a first class citizen of your application.
\ No newline at end of file
diff --git a/dotnet-tools.json b/dotnet-tools.json
index 57b0606..720fc54 100644
--- a/dotnet-tools.json
+++ b/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"cake.tool": {
- "version": "1.0.0-rc0001",
+ "version": "1.0.0-rc0002",
"commands": [
"dotnet-cake"
]
@@ -15,7 +15,7 @@
]
},
"dotnet-example": {
- "version": "1.1.0",
+ "version": "1.2.0",
"commands": [
"dotnet-example"
]
diff --git a/examples/Borders/Borders.csproj b/examples/Borders/Borders.csproj
deleted file mode 100644
index 492bbd5..0000000
--- a/examples/Borders/Borders.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Borders
- Demonstrates the different kind of borders.
-
-
-
-
-
-
-
diff --git a/examples/Calendars/Calendars.csproj b/examples/Calendars/Calendars.csproj
deleted file mode 100644
index 116dfa2..0000000
--- a/examples/Calendars/Calendars.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Calendars
- Demonstrates how to render calendars.
-
-
-
-
-
-
-
diff --git a/examples/Canvas/Canvas.csproj b/examples/Canvas/Canvas.csproj
deleted file mode 100644
index a98bd15..0000000
--- a/examples/Canvas/Canvas.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- Exe
- netcoreapp3.1
- false
- Canvas
- Demonstrates how to render pixels and images.
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
-
diff --git a/examples/Charts/Charts.csproj b/examples/Charts/Charts.csproj
deleted file mode 100644
index 5b16a50..0000000
--- a/examples/Charts/Charts.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Charts
- Demonstrates how to render charts in a console.
-
-
-
-
-
-
-
diff --git a/examples/Cli/Delegates/BarSettings.cs b/examples/Cli/Delegates/BarSettings.cs
new file mode 100644
index 0000000..75fa93c
--- /dev/null
+++ b/examples/Cli/Delegates/BarSettings.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace Delegates
+{
+ public static partial class Program
+ {
+ public sealed class BarSettings : CommandSettings
+ {
+ [CommandOption("--count")]
+ [Description("The number of bars to print")]
+ [DefaultValue(1)]
+ public int Count { get; set; }
+ }
+ }
+}
diff --git a/examples/Cli/Delegates/Delegates.csproj b/examples/Cli/Delegates/Delegates.csproj
new file mode 100644
index 0000000..4c6756c
--- /dev/null
+++ b/examples/Cli/Delegates/Delegates.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net5.0
+ false
+ Delegates
+ Demonstrates how to specify commands as delegates.
+ Cli
+ false
+
+
+
+
+
+
+
diff --git a/examples/Cli/Delegates/Program.cs b/examples/Cli/Delegates/Program.cs
new file mode 100644
index 0000000..6592372
--- /dev/null
+++ b/examples/Cli/Delegates/Program.cs
@@ -0,0 +1,39 @@
+using System;
+using Spectre.Console.Cli;
+
+namespace Delegates
+{
+ public static partial class Program
+ {
+ public static int Main(string[] args)
+ {
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.AddDelegate("foo", Foo)
+ .WithDescription("Foos the bars");
+
+ config.AddDelegate("bar", Bar)
+ .WithDescription("Bars the foos"); ;
+ });
+
+ return app.Run(args);
+ }
+
+ private static int Foo(CommandContext context)
+ {
+ Console.WriteLine("Foo");
+ return 0;
+ }
+
+ private static int Bar(CommandContext context, BarSettings settings)
+ {
+ for (var index = 0; index < settings.Count; index++)
+ {
+ Console.WriteLine("Bar");
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs b/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs
new file mode 100644
index 0000000..16fec31
--- /dev/null
+++ b/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using Demo.Utilities;
+using Spectre.Console.Cli;
+
+namespace Demo.Commands
+{
+ [Description("Add a NuGet package reference to the project.")]
+ public sealed class AddPackageCommand : Command
+ {
+ public sealed class Settings : AddSettings
+ {
+ [CommandArgument(0, "")]
+ [Description("The package reference to add.")]
+ public string PackageName { get; set; }
+
+ [CommandOption("-v|--version ")]
+ [Description("The version of the package to add.")]
+ public string Version { get; set; }
+
+ [CommandOption("-f|--framework ")]
+ [Description("Add the reference only when targeting a specific framework.")]
+ public string Framework { get; set; }
+
+ [CommandOption("--no-restore")]
+ [Description("Add the reference without performing restore preview and compatibility check.")]
+ public bool NoRestore { get; set; }
+
+ [CommandOption("--source ")]
+ [Description("The NuGet package source to use during the restore.")]
+ public string Source { get; set; }
+
+ [CommandOption("--package-directory ")]
+ [Description("The directory to restore packages to.")]
+ public string PackageDirectory { get; set; }
+
+ [CommandOption("--interactive")]
+ [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")]
+ public bool Interactive { get; set; }
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ SettingsDumper.Dump(settings);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs b/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs
new file mode 100644
index 0000000..6229743
--- /dev/null
+++ b/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel;
+using Demo.Utilities;
+using Spectre.Console.Cli;
+
+namespace Demo.Commands
+{
+ public sealed class AddReferenceCommand : Command
+ {
+ public sealed class Settings : AddSettings
+ {
+ [CommandArgument(0, "")]
+ [Description("The package reference to add.")]
+ public string ProjectPath { get; set; }
+
+ [CommandOption("-f|--framework ")]
+ [Description("Add the reference only when targeting a specific framework.")]
+ public string Framework { get; set; }
+
+ [CommandOption("--interactive")]
+ [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")]
+ public bool Interactive { get; set; }
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ SettingsDumper.Dump(settings);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Commands/Add/AddSettings.cs b/examples/Cli/Demo/Commands/Add/AddSettings.cs
new file mode 100644
index 0000000..95743ac
--- /dev/null
+++ b/examples/Cli/Demo/Commands/Add/AddSettings.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace Demo.Commands
+{
+ public abstract class AddSettings : CommandSettings
+ {
+ [CommandArgument(0, "")]
+ [Description("The project file to operate on. If a file is not specified, the command will search the current directory for one.")]
+ public string Project { get; set; }
+ }
+}
diff --git a/examples/Cli/Demo/Commands/Run/RunCommand.cs b/examples/Cli/Demo/Commands/Run/RunCommand.cs
new file mode 100644
index 0000000..772b572
--- /dev/null
+++ b/examples/Cli/Demo/Commands/Run/RunCommand.cs
@@ -0,0 +1,70 @@
+using System.ComponentModel;
+using Demo.Utilities;
+using Spectre.Console.Cli;
+
+namespace Demo.Commands
+{
+ [Description("Build and run a .NET project output.")]
+ public sealed class RunCommand : Command
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("-c|--configuration ")]
+ [Description("The configuration to run for. The default for most projects is '[grey]Debug[/]'.")]
+ [DefaultValue("Debug")]
+ public string Configuration { get; set; }
+
+ [CommandOption("-f|--framework ")]
+ [Description("The target framework to run for. The target framework must also be specified in the project file.")]
+ public string Framework { get; set; }
+
+ [CommandOption("-r|--runtime ")]
+ [Description("The target runtime to run for.")]
+ public string RuntimeIdentifier { get; set; }
+
+ [CommandOption("-p|--project ")]
+ [Description("The path to the project file to run (defaults to the current directory if there is only one project).")]
+ public string ProjectPath { get; set; }
+
+ [CommandOption("--launch-profile ")]
+ [Description("The name of the launch profile (if any) to use when launching the application.")]
+ public string LaunchProfile { get; set; }
+
+ [CommandOption("--no-launch-profile")]
+ [Description("Do not attempt to use [grey]launchSettings.json[/] to configure the application.")]
+ public bool NoLaunchProfile { get; set; }
+
+ [CommandOption("--no-build")]
+ [Description("Do not build the project before running. Implies [grey]--no-restore[/].")]
+ public bool NoBuild { get; set; }
+
+ [CommandOption("--interactive")]
+ [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")]
+ public string Interactive { get; set; }
+
+ [CommandOption("--no-restore")]
+ [Description("Do not restore the project before building.")]
+ public bool NoRestore { get; set; }
+
+ [CommandOption("--verbosity ")]
+ [Description("Set the MSBuild verbosity level. Allowed values are q[grey]uiet[/], m[grey]inimal[/], n[grey]ormal[/], d[grey]etailed[/], and diag[grey]nostic[/].")]
+ [TypeConverter(typeof(VerbosityConverter))]
+ [DefaultValue(Verbosity.Normal)]
+ public Verbosity Verbosity { get; set; }
+
+ [CommandOption("--no-dependencies")]
+ [Description("Do not restore project-to-project references and only restore the specified project.")]
+ public bool NoDependencies { get; set; }
+
+ [CommandOption("--force")]
+ [Description("Force all dependencies to be resolved even if the last restore was successful. This is equivalent to deleting [grey]project.assets.json[/].")]
+ public bool Force { get; set; }
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ SettingsDumper.Dump(settings);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Commands/Serve/ServeCommand.cs b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs
new file mode 100644
index 0000000..49298c5
--- /dev/null
+++ b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs
@@ -0,0 +1,41 @@
+using System;
+using System.ComponentModel;
+using Demo.Utilities;
+using Spectre.Console.Cli;
+
+namespace Demo.Commands
+{
+ [Description("Launches a web server in the current working directory and serves all files in it.")]
+ public sealed class ServeCommand : Command
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("-p|--port ")]
+ [Description("Port to use. Defaults to [grey]8080[/]. Use [grey]0[/] for a dynamic port.")]
+ public int Port { get; set; }
+
+ [CommandOption("-o|--open-browser [BROWSER]")]
+ [Description("Open a web browser when the server starts. You can also specify which browser to use. If none is specified, the default one will be used.")]
+ public FlagValue OpenBrowser { get; set; }
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ if (settings.OpenBrowser.IsSet)
+ {
+ var browser = settings.OpenBrowser.Value;
+ if (browser != null)
+ {
+ Console.WriteLine($"Open in {browser}");
+ }
+ else
+ {
+ Console.WriteLine($"Open in default browser.");
+ }
+ }
+
+ SettingsDumper.Dump(settings);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Demo.csproj b/examples/Cli/Demo/Demo.csproj
new file mode 100644
index 0000000..86b43be
--- /dev/null
+++ b/examples/Cli/Demo/Demo.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net5.0
+ false
+ Demo
+ Demonstrates the most common use cases of Spectre.Cli.
+ Cli
+ false
+
+
+
+
+
+
+
diff --git a/examples/Cli/Demo/Program.cs b/examples/Cli/Demo/Program.cs
new file mode 100644
index 0000000..e63fa04
--- /dev/null
+++ b/examples/Cli/Demo/Program.cs
@@ -0,0 +1,37 @@
+using Demo.Commands;
+using Spectre.Console.Cli;
+
+namespace Demo
+{
+ public static class Program
+ {
+ public static int Main(string[] args)
+ {
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.SetApplicationName("fake-dotnet");
+ config.ValidateExamples();
+ config.AddExample(new[] { "run", "--no-build" });
+
+ // Run
+ config.AddCommand("run");
+
+ // Add
+ config.AddBranch("add", add =>
+ {
+ add.SetDescription("Add a package or reference to a .NET project");
+ add.AddCommand("package");
+ add.AddCommand("reference");
+ });
+
+ // Serve
+ config.AddCommand("serve")
+ .WithExample(new[] { "serve", "-o", "firefox" })
+ .WithExample(new[] { "serve", "--port", "80", "-o", "firefox" });
+ });
+
+ return app.Run(args);
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Utilities/SettingsDumper.cs b/examples/Cli/Demo/Utilities/SettingsDumper.cs
new file mode 100644
index 0000000..f46d5cf
--- /dev/null
+++ b/examples/Cli/Demo/Utilities/SettingsDumper.cs
@@ -0,0 +1,29 @@
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace Demo.Utilities
+{
+ public static class SettingsDumper
+ {
+ public static void Dump(CommandSettings settings)
+ {
+ var table = new Table().RoundedBorder();
+ table.AddColumn("[grey]Name[/]");
+ table.AddColumn("[grey]Value[/]");
+
+ var properties = settings.GetType().GetProperties();
+ foreach (var property in properties)
+ {
+ var value = property.GetValue(settings)
+ ?.ToString()
+ ?.Replace("[", "[[");
+
+ table.AddRow(
+ property.Name,
+ value ?? "[grey]null[/]");
+ }
+
+ AnsiConsole.Render(table);
+ }
+ }
+}
diff --git a/examples/Cli/Demo/Verbosity.cs b/examples/Cli/Demo/Verbosity.cs
new file mode 100644
index 0000000..498a8c8
--- /dev/null
+++ b/examples/Cli/Demo/Verbosity.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Demo
+{
+ public enum Verbosity
+ {
+ Quiet,
+ Minimal,
+ Normal,
+ Detailed,
+ Diagnostic
+ }
+
+ public sealed class VerbosityConverter : TypeConverter
+ {
+ private readonly Dictionary _lookup;
+
+ public VerbosityConverter()
+ {
+ _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "q", Verbosity.Quiet },
+ { "quiet", Verbosity.Quiet },
+ { "m", Verbosity.Minimal },
+ { "minimal", Verbosity.Minimal },
+ { "n", Verbosity.Normal },
+ { "normal", Verbosity.Normal },
+ { "d", Verbosity.Detailed },
+ { "detailed", Verbosity.Detailed },
+ { "diag", Verbosity.Diagnostic },
+ { "diagnostic", Verbosity.Diagnostic }
+ };
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value is string stringValue)
+ {
+ var result = _lookup.TryGetValue(stringValue, out var verbosity);
+ if (!result)
+ {
+ const string format = "The value '{0}' is not a valid verbosity.";
+ var message = string.Format(CultureInfo.InvariantCulture, format, value);
+ throw new InvalidOperationException(message);
+ }
+ return verbosity;
+ }
+ throw new NotSupportedException("Can't convert value to verbosity.");
+ }
+ }
+}
diff --git a/examples/Cli/Dynamic/Dynamic.csproj b/examples/Cli/Dynamic/Dynamic.csproj
new file mode 100644
index 0000000..2066c58
--- /dev/null
+++ b/examples/Cli/Dynamic/Dynamic.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net5.0
+ false
+ Dynamic
+ Demonstrates how to define dynamic commands.
+ Cli
+ false
+
+
+
+
+
+
+
diff --git a/examples/Cli/Dynamic/MyCommand.cs b/examples/Cli/Dynamic/MyCommand.cs
new file mode 100644
index 0000000..b16caa8
--- /dev/null
+++ b/examples/Cli/Dynamic/MyCommand.cs
@@ -0,0 +1,20 @@
+using System;
+using Spectre.Console.Cli;
+
+namespace Dynamic
+{
+ public sealed class MyCommand : Command
+ {
+ public override int Execute(CommandContext context)
+ {
+ if (!(context.Data is int data))
+ {
+ throw new InvalidOperationException("Command has no associated data.");
+
+ }
+
+ Console.WriteLine("Value = {0}", data);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Dynamic/Program.cs b/examples/Cli/Dynamic/Program.cs
new file mode 100644
index 0000000..519fab7
--- /dev/null
+++ b/examples/Cli/Dynamic/Program.cs
@@ -0,0 +1,24 @@
+using System.Linq;
+using Spectre.Console.Cli;
+
+namespace Dynamic
+{
+ public static class Program
+ {
+ public static int Main(string[] args)
+ {
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ foreach(var index in Enumerable.Range(1, 10))
+ {
+ config.AddCommand($"c{index}")
+ .WithDescription($"Prints the number {index}")
+ .WithData(index);
+ }
+ });
+
+ return app.Run(args);
+ }
+ }
+}
diff --git a/examples/Cli/Injection/Commands/DefaultCommand.cs b/examples/Cli/Injection/Commands/DefaultCommand.cs
new file mode 100644
index 0000000..1ae059f
--- /dev/null
+++ b/examples/Cli/Injection/Commands/DefaultCommand.cs
@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace Injection.Commands
+{
+ public sealed class DefaultCommand : Command
+ {
+ private readonly IGreeter _greeter;
+
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("-n|--name ")]
+ [Description("The person or thing to greet.")]
+ [DefaultValue("World")]
+ public string Name { get; set; }
+ }
+
+ public DefaultCommand(IGreeter greeter)
+ {
+ _greeter = greeter ?? throw new ArgumentNullException(nameof(greeter));
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ _greeter.Greet(settings.Name);
+ return 0;
+ }
+ }
+}
diff --git a/examples/Cli/Injection/IGreeter.cs b/examples/Cli/Injection/IGreeter.cs
new file mode 100644
index 0000000..a2c285b
--- /dev/null
+++ b/examples/Cli/Injection/IGreeter.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Injection
+{
+ public interface IGreeter
+ {
+ void Greet(string name);
+ }
+
+ public sealed class HelloWorldGreeter : IGreeter
+ {
+ public void Greet(string name)
+ {
+ Console.WriteLine($"Hello {name}!");
+ }
+ }
+}
diff --git a/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs b/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs
new file mode 100644
index 0000000..0632649
--- /dev/null
+++ b/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs
@@ -0,0 +1,31 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
+
+namespace Injection
+{
+ public sealed class TypeRegistrar : ITypeRegistrar
+ {
+ private readonly IServiceCollection _builder;
+
+ public TypeRegistrar(IServiceCollection builder)
+ {
+ _builder = builder;
+ }
+
+ public ITypeResolver Build()
+ {
+ return new TypeResolver(_builder.BuildServiceProvider());
+ }
+
+ public void Register(Type service, Type implementation)
+ {
+ _builder.AddSingleton(service, implementation);
+ }
+
+ public void RegisterInstance(Type service, object implementation)
+ {
+ _builder.AddSingleton(service, implementation);
+ }
+ }
+}
diff --git a/examples/Cli/Injection/Infrastructure/TypeResolver.cs b/examples/Cli/Injection/Infrastructure/TypeResolver.cs
new file mode 100644
index 0000000..50320e2
--- /dev/null
+++ b/examples/Cli/Injection/Infrastructure/TypeResolver.cs
@@ -0,0 +1,21 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
+
+namespace Injection
+{
+ public sealed class TypeResolver : ITypeResolver
+ {
+ private readonly IServiceProvider _provider;
+
+ public TypeResolver(IServiceProvider provider)
+ {
+ _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ public object Resolve(Type type)
+ {
+ return _provider.GetRequiredService(type);
+ }
+ }
+}
diff --git a/examples/Cli/Injection/Injection.csproj b/examples/Cli/Injection/Injection.csproj
new file mode 100644
index 0000000..4c14b62
--- /dev/null
+++ b/examples/Cli/Injection/Injection.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net5.0
+ false
+ Injection
+ Demonstrates how to use dependency injection with Spectre.Cli.
+ Cli
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Cli/Injection/Program.cs b/examples/Cli/Injection/Program.cs
new file mode 100644
index 0000000..1206df9
--- /dev/null
+++ b/examples/Cli/Injection/Program.cs
@@ -0,0 +1,23 @@
+using Injection.Commands;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
+
+namespace Injection
+{
+ public class Program
+ {
+ public static int Main(string[] args)
+ {
+ // Create a type registrar and register any dependencies.
+ // A type registrar is an adapter for a DI framework.
+ var registrations = new ServiceCollection();
+ registrations.AddSingleton();
+ var registrar = new TypeRegistrar(registrations);
+
+ // Create a new command app with the registrar
+ // and run it with the provided arguments.
+ var app = new CommandApp(registrar);
+ return app.Run(args);
+ }
+ }
+}
diff --git a/examples/Colors/Colors.csproj b/examples/Colors/Colors.csproj
deleted file mode 100644
index 55ab209..0000000
--- a/examples/Colors/Colors.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Colors
- Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console.
-
-
-
-
-
-
-
diff --git a/examples/Console/Borders/Borders.csproj b/examples/Console/Borders/Borders.csproj
new file mode 100644
index 0000000..f51cc16
--- /dev/null
+++ b/examples/Console/Borders/Borders.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Borders
+ Demonstrates the different kind of borders.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Borders/Program.cs b/examples/Console/Borders/Program.cs
similarity index 100%
rename from examples/Borders/Program.cs
rename to examples/Console/Borders/Program.cs
diff --git a/examples/Console/Calendars/Calendars.csproj b/examples/Console/Calendars/Calendars.csproj
new file mode 100644
index 0000000..119a479
--- /dev/null
+++ b/examples/Console/Calendars/Calendars.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Calendars
+ Demonstrates how to render calendars.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Calendars/Program.cs b/examples/Console/Calendars/Program.cs
similarity index 100%
rename from examples/Calendars/Program.cs
rename to examples/Console/Calendars/Program.cs
diff --git a/examples/Console/Canvas/Canvas.csproj b/examples/Console/Canvas/Canvas.csproj
new file mode 100644
index 0000000..1a07982
--- /dev/null
+++ b/examples/Console/Canvas/Canvas.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ Canvas
+ Demonstrates how to render pixels and images.
+ Widgets
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/examples/Canvas/Mandelbrot.cs b/examples/Console/Canvas/Mandelbrot.cs
similarity index 100%
rename from examples/Canvas/Mandelbrot.cs
rename to examples/Console/Canvas/Mandelbrot.cs
diff --git a/examples/Canvas/Program.cs b/examples/Console/Canvas/Program.cs
similarity index 100%
rename from examples/Canvas/Program.cs
rename to examples/Console/Canvas/Program.cs
diff --git a/examples/Canvas/cake.png b/examples/Console/Canvas/cake.png
similarity index 100%
rename from examples/Canvas/cake.png
rename to examples/Console/Canvas/cake.png
diff --git a/examples/Console/Charts/Charts.csproj b/examples/Console/Charts/Charts.csproj
new file mode 100644
index 0000000..3763e82
--- /dev/null
+++ b/examples/Console/Charts/Charts.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Charts
+ Demonstrates how to render charts in a console.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Charts/Program.cs b/examples/Console/Charts/Program.cs
similarity index 100%
rename from examples/Charts/Program.cs
rename to examples/Console/Charts/Program.cs
diff --git a/examples/Console/Colors/Colors.csproj b/examples/Console/Colors/Colors.csproj
new file mode 100644
index 0000000..32185ad
--- /dev/null
+++ b/examples/Console/Colors/Colors.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Colors
+ Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Colors/Program.cs b/examples/Console/Colors/Program.cs
similarity index 100%
rename from examples/Colors/Program.cs
rename to examples/Console/Colors/Program.cs
diff --git a/examples/Colors/Utilities.cs b/examples/Console/Colors/Utilities.cs
similarity index 100%
rename from examples/Colors/Utilities.cs
rename to examples/Console/Colors/Utilities.cs
diff --git a/examples/Columns/Columns.csproj b/examples/Console/Columns/Columns.csproj
similarity index 52%
rename from examples/Columns/Columns.csproj
rename to examples/Console/Columns/Columns.csproj
index efbe910..474b855 100644
--- a/examples/Columns/Columns.csproj
+++ b/examples/Console/Columns/Columns.csproj
@@ -3,9 +3,9 @@
Exe
net5.0
- false
- Columns
- Demonstrates how to render data into columns.
+ Columns
+ Demonstrates how to render data into columns.
+ Widgets
@@ -13,7 +13,7 @@
-
+
diff --git a/examples/Columns/Program.cs b/examples/Console/Columns/Program.cs
similarity index 100%
rename from examples/Columns/Program.cs
rename to examples/Console/Columns/Program.cs
diff --git a/examples/Columns/User.cs b/examples/Console/Columns/User.cs
similarity index 100%
rename from examples/Columns/User.cs
rename to examples/Console/Columns/User.cs
diff --git a/examples/Console/Cursor/Cursor.csproj b/examples/Console/Cursor/Cursor.csproj
new file mode 100644
index 0000000..9daddac
--- /dev/null
+++ b/examples/Console/Cursor/Cursor.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Cursor
+ Demonstrates how to move the cursor.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Cursor/Program.cs b/examples/Console/Cursor/Program.cs
similarity index 100%
rename from examples/Cursor/Program.cs
rename to examples/Console/Cursor/Program.cs
diff --git a/examples/Console/Emojis/Emojis.csproj b/examples/Console/Emojis/Emojis.csproj
new file mode 100644
index 0000000..eceb0bd
--- /dev/null
+++ b/examples/Console/Emojis/Emojis.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Emojis
+ Demonstrates how to render emojis.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Emojis/Program.cs b/examples/Console/Emojis/Program.cs
similarity index 100%
rename from examples/Emojis/Program.cs
rename to examples/Console/Emojis/Program.cs
diff --git a/examples/Console/Exceptions/Exceptions.csproj b/examples/Console/Exceptions/Exceptions.csproj
new file mode 100644
index 0000000..128d8c0
--- /dev/null
+++ b/examples/Console/Exceptions/Exceptions.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Exceptions
+ Demonstrates how to render formatted exceptions.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs
similarity index 100%
rename from examples/Exceptions/Program.cs
rename to examples/Console/Exceptions/Program.cs
diff --git a/examples/Console/Figlet/Figlet.csproj b/examples/Console/Figlet/Figlet.csproj
new file mode 100644
index 0000000..fbac2d6
--- /dev/null
+++ b/examples/Console/Figlet/Figlet.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Figlet
+ Demonstrates how to render FIGlet text.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Figlet/Program.cs b/examples/Console/Figlet/Program.cs
similarity index 100%
rename from examples/Figlet/Program.cs
rename to examples/Console/Figlet/Program.cs
diff --git a/examples/Console/Grids/Grids.csproj b/examples/Console/Grids/Grids.csproj
new file mode 100644
index 0000000..95c9004
--- /dev/null
+++ b/examples/Console/Grids/Grids.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Grids
+ Demonstrates how to render grids in a console.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Grids/Program.cs b/examples/Console/Grids/Program.cs
similarity index 100%
rename from examples/Grids/Program.cs
rename to examples/Console/Grids/Program.cs
diff --git a/examples/Console/Info/Info.csproj b/examples/Console/Info/Info.csproj
new file mode 100644
index 0000000..f4ec75f
--- /dev/null
+++ b/examples/Console/Info/Info.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Info
+ Displays the capabilities of the current console.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Info/Program.cs b/examples/Console/Info/Program.cs
similarity index 100%
rename from examples/Info/Program.cs
rename to examples/Console/Info/Program.cs
diff --git a/examples/Console/Links/Links.csproj b/examples/Console/Links/Links.csproj
new file mode 100644
index 0000000..5054afa
--- /dev/null
+++ b/examples/Console/Links/Links.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Links
+ Demonstrates how to render links in a console.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Links/Program.cs b/examples/Console/Links/Program.cs
similarity index 100%
rename from examples/Links/Program.cs
rename to examples/Console/Links/Program.cs
diff --git a/examples/Console/Panels/Panels.csproj b/examples/Console/Panels/Panels.csproj
new file mode 100644
index 0000000..03add39
--- /dev/null
+++ b/examples/Console/Panels/Panels.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Panels
+ Demonstrates how to render items in panels.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Panels/Program.cs b/examples/Console/Panels/Program.cs
similarity index 100%
rename from examples/Panels/Program.cs
rename to examples/Console/Panels/Program.cs
diff --git a/examples/Progress/DescriptionGenerator.cs b/examples/Console/Progress/DescriptionGenerator.cs
similarity index 100%
rename from examples/Progress/DescriptionGenerator.cs
rename to examples/Console/Progress/DescriptionGenerator.cs
diff --git a/examples/Progress/Program.cs b/examples/Console/Progress/Program.cs
similarity index 100%
rename from examples/Progress/Program.cs
rename to examples/Console/Progress/Program.cs
diff --git a/examples/Progress/Progress.csproj b/examples/Console/Progress/Progress.csproj
similarity index 53%
rename from examples/Progress/Progress.csproj
rename to examples/Console/Progress/Progress.csproj
index f84c230..859b3f2 100644
--- a/examples/Progress/Progress.csproj
+++ b/examples/Console/Progress/Progress.csproj
@@ -3,9 +3,9 @@
Exe
net5.0
- false
- Progress
- Demonstrates how to show progress bars.
+ Progress
+ Demonstrates how to show progress bars.
+ Status
@@ -13,7 +13,7 @@
-
+
diff --git a/examples/Prompt/Program.cs b/examples/Console/Prompt/Program.cs
similarity index 100%
rename from examples/Prompt/Program.cs
rename to examples/Console/Prompt/Program.cs
diff --git a/examples/Console/Prompt/Prompt.csproj b/examples/Console/Prompt/Prompt.csproj
new file mode 100644
index 0000000..9c861c6
--- /dev/null
+++ b/examples/Console/Prompt/Prompt.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ 9
+ Prompt
+ Demonstrates how to get input from a user.
+ Misc
+
+
+
+
+
+
+
diff --git a/examples/Rules/Program.cs b/examples/Console/Rules/Program.cs
similarity index 100%
rename from examples/Rules/Program.cs
rename to examples/Console/Rules/Program.cs
diff --git a/examples/Console/Rules/Rules.csproj b/examples/Console/Rules/Rules.csproj
new file mode 100644
index 0000000..7d590fc
--- /dev/null
+++ b/examples/Console/Rules/Rules.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Rules
+ Demonstrates how to render horizontal rules (lines).
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Status/Program.cs b/examples/Console/Status/Program.cs
similarity index 100%
rename from examples/Status/Program.cs
rename to examples/Console/Status/Program.cs
diff --git a/examples/Status/Status.csproj b/examples/Console/Status/Status.csproj
similarity index 53%
rename from examples/Status/Status.csproj
rename to examples/Console/Status/Status.csproj
index 16128da..eb20f18 100644
--- a/examples/Status/Status.csproj
+++ b/examples/Console/Status/Status.csproj
@@ -3,9 +3,9 @@
Exe
net5.0
- false
- Status
- Demonstrates how to show status updates.
+ Status
+ Demonstrates how to show status updates.
+ Status
@@ -13,7 +13,7 @@
-
+
diff --git a/examples/Tables/Program.cs b/examples/Console/Tables/Program.cs
similarity index 100%
rename from examples/Tables/Program.cs
rename to examples/Console/Tables/Program.cs
diff --git a/examples/Console/Tables/Tables.csproj b/examples/Console/Tables/Tables.csproj
new file mode 100644
index 0000000..451f79a
--- /dev/null
+++ b/examples/Console/Tables/Tables.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net5.0
+ Tables
+ Demonstrates how to render tables in a console.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Cursor/Cursor.csproj b/examples/Cursor/Cursor.csproj
deleted file mode 100644
index 721c41b..0000000
--- a/examples/Cursor/Cursor.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Cursor
- Demonstrates how to move the cursor.
-
-
-
-
-
-
-
diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props
new file mode 100644
index 0000000..3080093
--- /dev/null
+++ b/examples/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+ false
+
+
\ No newline at end of file
diff --git a/examples/Emojis/Emojis.csproj b/examples/Emojis/Emojis.csproj
deleted file mode 100644
index 7fa39a9..0000000
--- a/examples/Emojis/Emojis.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Emojis
- Demonstrates how to render emojis.
-
-
-
-
-
-
-
diff --git a/examples/Exceptions/Exceptions.csproj b/examples/Exceptions/Exceptions.csproj
deleted file mode 100644
index 7ff8afa..0000000
--- a/examples/Exceptions/Exceptions.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Exceptions
- Demonstrates how to render formatted exceptions.
-
-
-
-
-
-
-
diff --git a/examples/Figlet/Figlet.csproj b/examples/Figlet/Figlet.csproj
deleted file mode 100644
index de0a616..0000000
--- a/examples/Figlet/Figlet.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Figlet
- Demonstrates how to render FIGlet text.
-
-
-
-
-
-
-
diff --git a/examples/Grids/Grids.csproj b/examples/Grids/Grids.csproj
deleted file mode 100644
index 914a1de..0000000
--- a/examples/Grids/Grids.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Grids
- Demonstrates how to render grids in a console.
-
-
-
-
-
-
-
diff --git a/examples/Info/Info.csproj b/examples/Info/Info.csproj
deleted file mode 100644
index bde96a4..0000000
--- a/examples/Info/Info.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Info
- Displays the capabilities of the current console.
-
-
-
-
-
-
-
diff --git a/examples/Links/Links.csproj b/examples/Links/Links.csproj
deleted file mode 100644
index 3b91568..0000000
--- a/examples/Links/Links.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Links
- Demonstrates how to render links in a console.
-
-
-
-
-
-
-
diff --git a/examples/Panels/Panels.csproj b/examples/Panels/Panels.csproj
deleted file mode 100644
index 21abf28..0000000
--- a/examples/Panels/Panels.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Panels
- Demonstrates how to render items in panels.
-
-
-
-
-
-
-
diff --git a/examples/Prompt/Prompt.csproj b/examples/Prompt/Prompt.csproj
deleted file mode 100644
index 21559d0..0000000
--- a/examples/Prompt/Prompt.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- Exe
- netcoreapp3.1
- 9
- false
- Prompt
- Demonstrates how to get input from a user.
-
-
-
-
-
-
-
diff --git a/examples/Rules/Rules.csproj b/examples/Rules/Rules.csproj
deleted file mode 100644
index 9522281..0000000
--- a/examples/Rules/Rules.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Rules
- Demonstrates how to render horizontal rules (lines).
-
-
-
-
-
-
-
diff --git a/examples/Tables/Tables.csproj b/examples/Tables/Tables.csproj
deleted file mode 100644
index 287b416..0000000
--- a/examples/Tables/Tables.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
- false
- Tables
- Demonstrates how to render tables in a console.
-
-
-
-
-
-
-
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index a1e8b22..bdf0b83 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -6,6 +6,7 @@
embedded
true
true
+ false
@@ -32,13 +33,13 @@
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
All
diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj
index 7e86188..5e2e05f 100644
--- a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj
+++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj
@@ -3,6 +3,7 @@
netstandard2.0
enable
+ true
A library that extends Spectre.Console with ImageSharp superpowers.
diff --git a/src/Spectre.Console.Testing/.editorconfig b/src/Spectre.Console.Testing/.editorconfig
new file mode 100644
index 0000000..70136ae
--- /dev/null
+++ b/src/Spectre.Console.Testing/.editorconfig
@@ -0,0 +1,14 @@
+root = false
+
+[*.cs]
+# CS1591: Missing XML comment for publicly visible type or member
+dotnet_diagnostic.CS1591.severity = none
+
+# SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = none
+
+# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.OrderingRules'
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = none
+
+# CA1819: Properties should not return arrays
+dotnet_diagnostic.CA1819.severity = none
\ No newline at end of file
diff --git a/src/Spectre.Console.Testing/CommandAppFixture.cs b/src/Spectre.Console.Testing/CommandAppFixture.cs
new file mode 100644
index 0000000..f26e364
--- /dev/null
+++ b/src/Spectre.Console.Testing/CommandAppFixture.cs
@@ -0,0 +1,92 @@
+using System;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Testing
+{
+ public sealed class CommandAppFixture
+ {
+ private Action _appConfiguration = _ => { };
+ private Action _configuration;
+
+ public CommandAppFixture()
+ {
+ _configuration = (_) => { };
+ }
+
+ public CommandAppFixture WithDefaultCommand()
+ where T : class, ICommand
+ {
+ _appConfiguration = (app) => app.SetDefaultCommand();
+ return this;
+ }
+
+ public void Configure(Action action)
+ {
+ _configuration = action;
+ }
+
+ public (string Message, string Output) RunAndCatch(params string[] args)
+ where T : Exception
+ {
+ CommandContext context = null;
+ CommandSettings settings = null;
+
+ using var console = new FakeConsole();
+
+ var app = new CommandApp();
+ _appConfiguration?.Invoke(app);
+
+ app.Configure(_configuration);
+ app.Configure(c => c.ConfigureConsole(console));
+ app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) =>
+ {
+ context = ctx;
+ settings = s;
+ })));
+
+ try
+ {
+ app.Run(args);
+ }
+ catch (T ex)
+ {
+ var output = console.Output
+ .NormalizeLineEndings()
+ .TrimLines()
+ .Trim();
+
+ return (ex.Message, output);
+ }
+
+ throw new InvalidOperationException("No exception was thrown");
+ }
+
+ public (int ExitCode, string Output, CommandContext Context, CommandSettings Settings) Run(params string[] args)
+ {
+ CommandContext context = null;
+ CommandSettings settings = null;
+
+ using var console = new FakeConsole(width: int.MaxValue);
+
+ var app = new CommandApp();
+ _appConfiguration?.Invoke(app);
+
+ app.Configure(_configuration);
+ app.Configure(c => c.ConfigureConsole(console));
+ app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) =>
+ {
+ context = ctx;
+ settings = s;
+ })));
+
+ var result = app.Run(args);
+
+ var output = console.Output
+ .NormalizeLineEndings()
+ .TrimLines()
+ .Trim();
+
+ return (result, output, context, settings);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Testing/EmbeddedResourceReader.cs b/src/Spectre.Console.Testing/EmbeddedResourceReader.cs
new file mode 100644
index 0000000..0b3fe21
--- /dev/null
+++ b/src/Spectre.Console.Testing/EmbeddedResourceReader.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace Spectre.Console.Tests
+{
+ public static class EmbeddedResourceReader
+ {
+ public static Stream LoadResourceStream(string resourceName)
+ {
+ if (resourceName is null)
+ {
+ throw new ArgumentNullException(nameof(resourceName));
+ }
+
+ var assembly = Assembly.GetCallingAssembly();
+ resourceName = resourceName.ReplaceExact("/", ".");
+
+ return assembly.GetManifestResourceStream(resourceName);
+ }
+
+ public static Stream LoadResourceStream(Assembly assembly, string resourceName)
+ {
+ if (assembly is null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ if (resourceName is null)
+ {
+ throw new ArgumentNullException(nameof(resourceName));
+ }
+
+ resourceName = resourceName.ReplaceExact("/", ".");
+ return assembly.GetManifestResourceStream(resourceName);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs b/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs
new file mode 100644
index 0000000..16b41ae
--- /dev/null
+++ b/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Linq;
+using Shouldly;
+
+namespace Spectre.Console.Cli
+{
+ public static class CommandContextExtensions
+ {
+ public static void ShouldHaveRemainingArgument(this CommandContext context, string name, string[] values)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ context.Remaining.Parsed.Contains(name).ShouldBeTrue();
+ context.Remaining.Parsed[name].Count().ShouldBe(values.Length);
+
+ foreach (var value in values)
+ {
+ context.Remaining.Parsed[name].ShouldContain(value);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs b/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs
new file mode 100644
index 0000000..4701acc
--- /dev/null
+++ b/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Diagnostics;
+using Shouldly;
+
+namespace Spectre.Console
+{
+ public static class ShouldlyExtensions
+ {
+ [DebuggerStepThrough]
+ public static T And(this T item, Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ action(item);
+ return item;
+ }
+
+ [DebuggerStepThrough]
+ public static void As(this T item, Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ action(item);
+ }
+
+ [DebuggerStepThrough]
+ public static void As(this object item, Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ action((T)item);
+ }
+
+ [DebuggerStepThrough]
+ public static void ShouldBe(this Type item)
+ {
+ item.ShouldBe(typeof(T));
+ }
+ }
+}
diff --git a/src/Spectre.Console.Testing/Extensions/StringExtensions.cs b/src/Spectre.Console.Testing/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..05ac9e3
--- /dev/null
+++ b/src/Spectre.Console.Testing/Extensions/StringExtensions.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Spectre.Console
+{
+ public static class StringExtensions
+ {
+ private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
+ private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
+
+ public static string TrimLines(this string value)
+ {
+ if (value is null)
+ {
+ return string.Empty;
+ }
+
+ var result = new List();
+ var lines = value.Split(new[] { '\n' });
+
+ foreach (var line in lines)
+ {
+ var current = line.TrimEnd();
+ if (string.IsNullOrWhiteSpace(current))
+ {
+ result.Add(string.Empty);
+ }
+ else
+ {
+ result.Add(current);
+ }
+ }
+
+ return string.Join("\n", result);
+ }
+
+ public static string NormalizeLineEndings(this string value)
+ {
+ if (value != null)
+ {
+ value = value.Replace("\r\n", "\n");
+ return value.Replace("\r", string.Empty);
+ }
+
+ return string.Empty;
+ }
+
+ public static string NormalizeStackTrace(this string text)
+ {
+ text = _lineNumberRegex.Replace(text, match =>
+ {
+ return ":nn";
+ });
+
+ return _filenameRegex.Replace(text, match =>
+ {
+ var value = match.Value;
+ var index = value.LastIndexOfAny(new[] { '\\', '/' });
+ var filename = value.Substring(index + 1, value.Length - index - 1);
+
+ return $" in /xyz/{filename}";
+ });
+ }
+
+ internal static string ReplaceExact(this string text, string oldValue, string newValue)
+ {
+ if (string.IsNullOrWhiteSpace(newValue))
+ {
+ return text;
+ }
+
+#if NET5_0
+ return text.Replace(oldValue, newValue, StringComparison.Ordinal);
+#else
+ return text.Replace(oldValue, newValue);
+#endif
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Extensions/StyleExtensions.cs b/src/Spectre.Console.Testing/Extensions/StyleExtensions.cs
similarity index 87%
rename from src/Spectre.Console.Tests/Extensions/StyleExtensions.cs
rename to src/Spectre.Console.Testing/Extensions/StyleExtensions.cs
index d64c9ac..cf8d466 100644
--- a/src/Spectre.Console.Tests/Extensions/StyleExtensions.cs
+++ b/src/Spectre.Console.Testing/Extensions/StyleExtensions.cs
@@ -1,6 +1,6 @@
namespace Spectre.Console.Tests
{
- internal static class StyleExtensions
+ public static class StyleExtensions
{
public static Style SetColor(this Style style, Color color, bool foreground)
{
diff --git a/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs b/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs
new file mode 100644
index 0000000..8bbee95
--- /dev/null
+++ b/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Xml;
+
+namespace Spectre.Console
+{
+ public static class XmlElementExtensions
+ {
+ public static void SetNullableAttribute(this XmlElement element, string name, string value)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ element.SetAttribute(name, value ?? "NULL");
+ }
+
+ public static void SetNullableAttribute(this XmlElement element, string name, IEnumerable values)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ if (values?.Any() != true)
+ {
+ element.SetAttribute(name, "NULL");
+ }
+
+ element.SetAttribute(name, string.Join(",", values));
+ }
+
+ public static void SetBooleanAttribute(this XmlElement element, string name, bool value)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ element.SetAttribute(name, value ? "true" : "false");
+ }
+
+ public static void SetEnumAttribute(this XmlElement element, string name, Enum value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var field = value.GetType().GetField(value.ToString());
+ var attribute = field.GetCustomAttribute(false);
+ if (attribute == null)
+ {
+ throw new InvalidOperationException("Enum is missing description.");
+ }
+
+ element.SetAttribute(name, attribute.Description);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs
similarity index 83%
rename from src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs
rename to src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs
index 48af9d4..f92ea46 100644
--- a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs
+++ b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs
@@ -4,9 +4,9 @@ using System.IO;
using System.Text;
using Spectre.Console.Rendering;
-namespace Spectre.Console.Tests
+namespace Spectre.Console.Testing
{
- public sealed class TestableAnsiConsole : IDisposable, IAnsiConsole
+ public sealed class FakeAnsiConsole : IDisposable, IAnsiConsole
{
private readonly StringWriter _writer;
private readonly IAnsiConsole _console;
@@ -18,12 +18,12 @@ namespace Spectre.Console.Tests
public int Width { get; }
public int Height => _console.Height;
public IAnsiConsoleCursor Cursor => _console.Cursor;
- public TestableConsoleInput Input { get; }
+ public FakeConsoleInput Input { get; }
public RenderPipeline Pipeline => _console.Pipeline;
IAnsiConsoleInput IAnsiConsole.Input => Input;
- public TestableAnsiConsole(
+ public FakeAnsiConsole(
ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes,
InteractionSupport interaction = InteractionSupport.Yes,
int width = 80)
@@ -35,11 +35,11 @@ namespace Spectre.Console.Tests
ColorSystem = (ColorSystemSupport)system,
Interactive = interaction,
Out = _writer,
- LinkIdentityGenerator = new TestLinkIdentityGenerator(),
+ LinkIdentityGenerator = new FakeLinkIdentityGenerator(1024),
});
Width = width;
- Input = new TestableConsoleInput();
+ Input = new FakeConsoleInput();
}
public void Dispose()
diff --git a/src/Spectre.Console.Tests/Tools/DummyCursor.cs b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs
similarity index 69%
rename from src/Spectre.Console.Tests/Tools/DummyCursor.cs
rename to src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs
index fe1f75a..a67f506 100644
--- a/src/Spectre.Console.Tests/Tools/DummyCursor.cs
+++ b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs
@@ -1,6 +1,6 @@
-namespace Spectre.Console.Tests
+namespace Spectre.Console.Testing
{
- public sealed class DummyCursor : IAnsiConsoleCursor
+ public sealed class FakeAnsiConsoleCursor : IAnsiConsoleCursor
{
public void Move(CursorDirection direction, int steps)
{
diff --git a/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs b/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs
new file mode 100644
index 0000000..254a8a8
--- /dev/null
+++ b/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs
@@ -0,0 +1,20 @@
+using System;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Testing
+{
+ public sealed class FakeCommandInterceptor : ICommandInterceptor
+ {
+ private readonly Action _action;
+
+ public FakeCommandInterceptor(Action action)
+ {
+ _action = action ?? throw new ArgumentNullException(nameof(action));
+ }
+
+ public void Intercept(CommandContext context, CommandSettings settings)
+ {
+ _action(context, settings);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Tools/PlainConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs
similarity index 88%
rename from src/Spectre.Console.Tests/Tools/PlainConsole.cs
rename to src/Spectre.Console.Testing/Fakes/FakeConsole.cs
index 105d056..048b758 100644
--- a/src/Spectre.Console.Tests/Tools/PlainConsole.cs
+++ b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs
@@ -5,14 +5,14 @@ using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
-namespace Spectre.Console.Tests
+namespace Spectre.Console.Testing
{
- public sealed class PlainConsole : IAnsiConsole, IDisposable
+ public sealed class FakeConsole : IAnsiConsole, IDisposable
{
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
- public IAnsiConsoleCursor Cursor => new DummyCursor();
- public TestableConsoleInput Input { get; }
+ public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor();
+ public FakeConsoleInput Input { get; }
public int Width { get; }
public int Height { get; }
@@ -29,7 +29,7 @@ namespace Spectre.Console.Tests
public string Output => Writer.ToString();
public IReadOnlyList Lines => Output.TrimEnd('\n').Split(new char[] { '\n' });
- public PlainConsole(
+ public FakeConsole(
int width = 80, int height = 9000, Encoding encoding = null,
bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard,
bool legacyConsole = false, bool interactive = true)
@@ -39,7 +39,7 @@ namespace Spectre.Console.Tests
Width = width;
Height = height;
Writer = new StringWriter();
- Input = new TestableConsoleInput();
+ Input = new FakeConsoleInput();
Pipeline = new RenderPipeline();
}
diff --git a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs b/src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs
similarity index 90%
rename from src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs
rename to src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs
index 0764be4..a3223fe 100644
--- a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs
+++ b/src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs
@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
-namespace Spectre.Console.Tests
+namespace Spectre.Console.Testing
{
- public sealed class TestableConsoleInput : IAnsiConsoleInput
+ public sealed class FakeConsoleInput : IAnsiConsoleInput
{
private readonly Queue _input;
- public TestableConsoleInput()
+ public FakeConsoleInput()
{
_input = new Queue();
}
diff --git a/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs b/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs
new file mode 100644
index 0000000..41f9443
--- /dev/null
+++ b/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs
@@ -0,0 +1,17 @@
+namespace Spectre.Console.Testing
+{
+ public sealed class FakeLinkIdentityGenerator : ILinkIdentityGenerator
+ {
+ private readonly int _linkId;
+
+ public FakeLinkIdentityGenerator(int linkId)
+ {
+ _linkId = linkId;
+ }
+
+ public int GenerateId(string link, string text)
+ {
+ return _linkId;
+ }
+ }
+}
diff --git a/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs b/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs
new file mode 100644
index 0000000..440856b
--- /dev/null
+++ b/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Testing
+{
+ public sealed class FakeTypeRegistrar : ITypeRegistrar
+ {
+ private readonly ITypeResolver _resolver;
+ public Dictionary> Registrations { get; }
+ public Dictionary> Instances { get; }
+
+ public FakeTypeRegistrar(ITypeResolver resolver = null)
+ {
+ _resolver = resolver;
+ Registrations = new Dictionary>();
+ Instances = new Dictionary>();
+ }
+
+ public void Register(Type service, Type implementation)
+ {
+ if (!Registrations.ContainsKey(service))
+ {
+ Registrations.Add(service, new List { implementation });
+ }
+ else
+ {
+ Registrations[service].Add(implementation);
+ }
+ }
+
+ public void RegisterInstance(Type service, object implementation)
+ {
+ if (!Instances.ContainsKey(service))
+ {
+ Instances.Add(service, new List
+
diff --git a/src/Spectre.Console.Tests/Tools/DummySpinners.cs b/src/Spectre.Console.Tests/Tools/DummySpinners.cs
deleted file mode 100644
index 63444c9..0000000
--- a/src/Spectre.Console.Tests/Tools/DummySpinners.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Spectre.Console.Tests
-{
- public sealed class DummySpinner1 : Spinner
- {
- public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
- public override bool IsUnicode => true;
- public override IReadOnlyList Frames => new List
- {
- "*",
- };
- }
-
- public sealed class DummySpinner2 : Spinner
- {
- public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
- public override bool IsUnicode => true;
- public override IReadOnlyList Frames => new List
- {
- "-",
- };
- }
-}
diff --git a/src/Spectre.Console.Tests/Tools/ResourceReader.cs b/src/Spectre.Console.Tests/Tools/ResourceReader.cs
deleted file mode 100644
index 9a09c72..0000000
--- a/src/Spectre.Console.Tests/Tools/ResourceReader.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.IO;
-
-namespace Spectre.Console.Tests
-{
- public static class ResourceReader
- {
- public static Stream LoadResourceStream(string resourceName)
- {
- if (resourceName is null)
- {
- throw new ArgumentNullException(nameof(resourceName));
- }
-
- var assembly = typeof(EmbeddedResourceDataAttribute).Assembly;
- resourceName = resourceName.Replace("/", ".", StringComparison.Ordinal);
-
- return assembly.GetManifestResourceStream(resourceName);
- }
- }
-}
diff --git a/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs b/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs
deleted file mode 100644
index 0a34220..0000000
--- a/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Spectre.Console.Tests
-{
- public sealed class TestLinkIdentityGenerator : ILinkIdentityGenerator
- {
- public int GenerateId(string link, string text)
- {
- return 1024;
- }
- }
-}
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs
index d7c32bf..8fc9970 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs
@@ -1,4 +1,5 @@
using Shouldly;
+using Spectre.Console.Testing;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -13,7 +14,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Return_Correct_Code(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.TrueColor);
+ var console = new FakeAnsiConsole(ColorSystem.TrueColor);
// When
console.Write("Hello", new Style().SetColor(new Color(128, 0, 128), foreground));
@@ -28,7 +29,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Return_Eight_Bit_Ansi_Code_For_Known_Colors(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.TrueColor);
+ var console = new FakeAnsiConsole(ColorSystem.TrueColor);
// When
console.Write("Hello", new Style().SetColor(Color.Purple, foreground));
@@ -46,7 +47,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.EightBit);
+ var console = new FakeAnsiConsole(ColorSystem.EightBit);
// When
console.Write("Hello", new Style().SetColor(Color.Olive, foreground));
@@ -61,7 +62,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Map_TrueColor_To_Nearest_Eight_Bit_Color_If_Possible(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.EightBit);
+ var console = new FakeAnsiConsole(ColorSystem.EightBit);
// When
console.Write("Hello", new Style().SetColor(new Color(128, 128, 0), foreground));
@@ -76,7 +77,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Estimate_TrueColor_To_Nearest_Eight_Bit_Color(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.EightBit);
+ var console = new FakeAnsiConsole(ColorSystem.EightBit);
// When
console.Write("Hello", new Style().SetColor(new Color(126, 127, 0), foreground));
@@ -94,7 +95,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write("Hello", new Style().SetColor(Color.Olive, foreground));
@@ -114,7 +115,7 @@ namespace Spectre.Console.Tests.Unit
string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground));
@@ -134,7 +135,7 @@ namespace Spectre.Console.Tests.Unit
string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground));
@@ -152,7 +153,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Legacy);
+ var console = new FakeAnsiConsole(ColorSystem.Legacy);
// When
console.Write("Hello", new Style().SetColor(Color.Olive, foreground));
@@ -172,7 +173,7 @@ namespace Spectre.Console.Tests.Unit
string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Legacy);
+ var console = new FakeAnsiConsole(ColorSystem.Legacy);
// When
console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground));
@@ -192,7 +193,7 @@ namespace Spectre.Console.Tests.Unit
string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Legacy);
+ var console = new FakeAnsiConsole(ColorSystem.Legacy);
// When
console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground));
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
index 1ed652b..f8d3006 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Shouldly;
+using Spectre.Console.Testing;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -18,7 +19,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
console.Markup(markup);
@@ -32,7 +33,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Be_Able_To_Escape_Tags(string markup, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
console.Markup(markup);
@@ -49,7 +50,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Throw_If_Encounters_Malformed_Tag(string markup, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
var result = Record.Exception(() => console.Markup(markup));
@@ -63,7 +64,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Throw_If_Tags_Are_Unbalanced()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
var result = Record.Exception(() => console.Markup("[yellow][blue]Hello[/]"));
@@ -77,7 +78,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Throw_If_Encounters_Closing_Tag()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
var result = Record.Exception(() => console.Markup("Hello[/]World"));
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
index 3b41a40..8424d03 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
@@ -1,4 +1,5 @@
using Shouldly;
+using Spectre.Console.Testing;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -18,7 +19,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Write_Decorated_Text_Correctly(Decoration decoration, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.TrueColor);
+ var console = new FakeAnsiConsole(ColorSystem.TrueColor);
// When
console.Write("Hello World", new Style().Decoration(decoration));
@@ -33,7 +34,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Write_Text_With_Multiple_Decorations_Correctly(Decoration decoration, string expected)
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.TrueColor);
+ var console = new FakeAnsiConsole(ColorSystem.TrueColor);
// When
console.Write("Hello World", new Style().Decoration(decoration));
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
index b63597f..9d2ab16 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
@@ -1,5 +1,6 @@
using System;
using Shouldly;
+using Spectre.Console.Testing;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -10,7 +11,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Combine_Decoration_And_Colors()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write(
@@ -28,7 +29,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Not_Include_Foreground_If_Set_To_Default_Color()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write(
@@ -46,7 +47,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Not_Include_Background_If_Set_To_Default_Color()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write(
@@ -64,7 +65,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Not_Include_Decoration_If_Set_To_None()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard);
+ var console = new FakeAnsiConsole(ColorSystem.Standard);
// When
console.Write(
@@ -84,7 +85,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Reset_Colors_Correctly_After_Line_Break()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
console.WriteLine("Hello", new Style().Background(ConsoleColor.Red));
@@ -99,7 +100,7 @@ namespace Spectre.Console.Tests.Unit
public void Should_Reset_Colors_Correctly_After_Line_Break_In_Text()
{
// Given
- var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
+ var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes);
// When
console.WriteLine("Hello\nWorld", new Style().Background(ConsoleColor.Red));
diff --git a/src/Spectre.Console.Tests/Unit/BarChartTests.cs b/src/Spectre.Console.Tests/Unit/BarChartTests.cs
index 558cf52..50b46c9 100644
--- a/src/Spectre.Console.Tests/Unit/BarChartTests.cs
+++ b/src/Spectre.Console.Tests/Unit/BarChartTests.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using Spectre.Console.Testing;
using VerifyXunit;
using Xunit;
@@ -11,7 +12,7 @@ namespace Spectre.Console.Tests.Unit
public async Task Should_Render_Correctly()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
// When
console.Render(new BarChart()
diff --git a/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs b/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs
index ed48994..ebd86a6 100644
--- a/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs
+++ b/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Shouldly;
using Spectre.Console.Rendering;
+using Spectre.Console.Testing;
using VerifyXunit;
using Xunit;
@@ -29,7 +30,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().NoBorder();
// When
@@ -60,7 +61,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().AsciiBorder();
// When
@@ -91,7 +92,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().DoubleBorder();
// When
@@ -122,7 +123,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().HeavyBorder();
// When
@@ -150,7 +151,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().RoundedBorder();
// When
@@ -178,7 +179,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_As_Expected()
{
// Given
- var console = new PlainConsole();
+ var console = new FakeConsole();
var panel = Fixture.GetPanel().SquareBorder();
// When
diff --git a/src/Spectre.Console.Tests/Unit/CalendarTests.cs b/src/Spectre.Console.Tests/Unit/CalendarTests.cs
index 0c8d6a6..9dd7dac 100644
--- a/src/Spectre.Console.Tests/Unit/CalendarTests.cs
+++ b/src/Spectre.Console.Tests/Unit/CalendarTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using Spectre.Console.Testing;
using VerifyXunit;
using Xunit;
@@ -12,7 +13,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_Calendar_Correctly()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
var calendar = new Calendar(2020, 10)
.AddCalendarEvent(new DateTime(2020, 9, 1))
.AddCalendarEvent(new DateTime(2020, 10, 3))
@@ -29,7 +30,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Center_Calendar_Correctly()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
var calendar = new Calendar(2020, 10)
.Centered()
.AddCalendarEvent(new DateTime(2020, 9, 1))
@@ -47,7 +48,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Left_Align_Calendar_Correctly()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
var calendar = new Calendar(2020, 10)
.LeftAligned()
.AddCalendarEvent(new DateTime(2020, 9, 1))
@@ -65,7 +66,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Right_Align_Calendar_Correctly()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
var calendar = new Calendar(2020, 10)
.RightAligned()
.AddCalendarEvent(new DateTime(2020, 9, 1))
@@ -83,7 +84,7 @@ namespace Spectre.Console.Tests.Unit
public Task Should_Render_Calendar_Correctly_For_Specific_Culture()
{
// Given
- var console = new PlainConsole(width: 80);
+ var console = new FakeConsole(width: 80);
var calendar = new Calendar(2020, 10, 15)
.Culture("de-DE")
.AddCalendarEvent(new DateTime(2020, 9, 1))
diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs
new file mode 100644
index 0000000..73b4232
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs
@@ -0,0 +1,92 @@
+using System.Threading.Tasks;
+using Spectre.Console.Cli;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using VerifyXunit;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli.Annotations
+{
+ public sealed partial class CommandArgumentAttributeTests
+ {
+ [UsesVerify]
+ public sealed class TheArgumentCannotContainOptionsMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandArgument(0, "--foo ")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var result = Fixture.Run();
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheMultipleValuesAreNotSupportedMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandArgument(0, " ")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var result = Fixture.Run();
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheValuesMustHaveNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandArgument(0, "<>")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var result = Fixture.Run();
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ private static class Fixture
+ {
+ public static string Run(params string[] args)
+ where TSettings : CommandSettings
+ {
+ using (var writer = new FakeConsole())
+ {
+ var app = new CommandApp();
+ app.Configure(c => c.ConfigureConsole(writer));
+ app.Configure(c => c.AddCommand>("foo"));
+ app.Run(args);
+
+ return writer.Output
+ .NormalizeLineEndings()
+ .TrimLines()
+ .Trim();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs
new file mode 100644
index 0000000..b4689fd
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs
@@ -0,0 +1,64 @@
+using Shouldly;
+using Spectre.Console.Cli;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli.Annotations
+{
+ public sealed partial class CommandArgumentAttributeTests
+ {
+ [Fact]
+ public void Should_Not_Contain_Options()
+ {
+ // Given, When
+ var result = Record.Exception(() => new CommandArgumentAttribute(0, "--foo "));
+
+ // Then
+ result.ShouldNotBe(null);
+ result.ShouldBeOfType().And(exception =>
+ exception.Message.ShouldBe("Arguments can not contain options."));
+ }
+
+ [Theory]
+ [InlineData(" ")]
+ [InlineData("[FOO] [BAR]")]
+ [InlineData("[FOO] ")]
+ [InlineData(" [BAR]")]
+ public void Should_Not_Contain_Multiple_Value_Names(string template)
+ {
+ // Given, When
+ var result = Record.Exception(() => new CommandArgumentAttribute(0, template));
+
+ // Then
+ result.ShouldNotBe(null);
+ result.ShouldBeOfType().And(exception =>
+ exception.Message.ShouldBe("Multiple values are not supported."));
+ }
+
+ [Theory]
+ [InlineData("<>")]
+ [InlineData("[]")]
+ public void Should_Not_Contain_Empty_Value_Name(string template)
+ {
+ // Given, When
+ var result = Record.Exception(() => new CommandArgumentAttribute(0, template));
+
+ // Then
+ result.ShouldNotBe(null);
+ result.ShouldBeOfType().And(exception =>
+ exception.Message.ShouldBe("Values without name are not allowed."));
+ }
+
+ [Theory]
+ [InlineData("", true)]
+ [InlineData("[FOO]", false)]
+ public void Should_Parse_Valid_Options(string template, bool required)
+ {
+ // Given, When
+ var result = new CommandArgumentAttribute(0, template);
+
+ // Then
+ result.ValueName.ShouldBe("FOO");
+ result.IsRequired.ShouldBe(required);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs
new file mode 100644
index 0000000..4185369
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs
@@ -0,0 +1,239 @@
+using System.Threading.Tasks;
+using Shouldly;
+using Spectre.Console.Cli;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using VerifyXunit;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli.Annotations
+{
+ public sealed partial class CommandOptionAttributeTests
+ {
+ [UsesVerify]
+ public sealed class TheUnexpectedCharacterMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption(" $ ")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Encountered unexpected character '$'.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheUnterminatedValueNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--foo|-f ();
+
+ // Then
+ message.ShouldBe("Encountered unterminated value name 'BAR'.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheOptionsMustHaveNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--foo|-")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Options without name are not allowed.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheOptionNamesCannotStartWithDigitMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--1foo")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Option names cannot start with a digit.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheInvalidCharacterInOptionNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--f$oo")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Encountered invalid character '$' in option name.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheLongOptionMustHaveMoreThanOneCharacterMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--f")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Long option names must consist of more than one character.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheShortOptionMustOnlyBeOneCharacterMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("--foo|-bar")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Short option names can not be longer than one character.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheMultipleOptionValuesAreNotSupportedMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("-f|--foo ")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Multiple option values are not supported.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheInvalidCharacterInValueNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("-f|--foo ")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("Encountered invalid character '$' in value name.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class TheMissingLongAndShortNameMethod
+ {
+ public sealed class Settings : CommandSettings
+ {
+ [CommandOption("")]
+ public string Foo { get; set; }
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given, When
+ var (message, result) = Fixture.Run();
+
+ // Then
+ message.ShouldBe("No long or short name for option has been specified.");
+ return Verifier.Verify(result);
+ }
+ }
+
+ private static class Fixture
+ {
+ public static (string Message, string Output) Run(params string[] args)
+ where TSettings : CommandSettings
+ {
+ var app = new CommandAppFixture();
+ app.Configure(c =>
+ {
+ c.PropagateExceptions();
+ c.AddCommand>("foo");
+ });
+
+ return app.RunAndCatch(args);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs
new file mode 100644
index 0000000..d9efb57
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs
@@ -0,0 +1,197 @@
+using Shouldly;
+using Spectre.Console.Cli;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli.Annotations
+{
+ public sealed partial class CommandOptionAttributeTests
+ {
+ [Fact]
+ public void Should_Parse_Short_Name_Correctly()
+ {
+ // Given, When
+ var option = new CommandOptionAttribute("-o|--option ");
+
+ // Then
+ option.ShortNames.ShouldContain("o");
+ }
+
+ [Fact]
+ public void Should_Parse_Long_Name_Correctly()
+ {
+ // Given, When
+ var option = new CommandOptionAttribute("-o|--option ");
+
+ // Then
+ option.LongNames.ShouldContain("option");
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void Should_Parse_Value_Correctly(string value)
+ {
+ // Given, When
+ var option = new CommandOptionAttribute($"-o|--option {value}");
+
+ // Then
+ option.ValueName.ShouldBe("VALUE");
+ }
+
+ [Fact]
+ public void Should_Parse_Only_Short_Name()
+ {
+ // Given, When
+ var option = new CommandOptionAttribute("-o");
+
+ // Then
+ option.ShortNames.ShouldContain("o");
+ }
+
+ [Fact]
+ public void Should_Parse_Only_Long_Name()
+ {
+ // Given, When
+ var option = new CommandOptionAttribute("--option");
+
+ // Then
+ option.LongNames.ShouldContain("option");
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("")]
+ public void Should_Throw_If_Template_Is_Empty(string value)
+ {
+ // Given, When
+ var option = Record.Exception(() => new CommandOptionAttribute(value));
+
+ // Then
+ option.ShouldBeOfType().And(e =>
+ e.Message.ShouldBe("No long or short name for option has been specified."));
+ }
+
+ [Theory]
+ [InlineData("--bar|-foo")]
+ [InlineData("--bar|-f-b")]
+ public void Should_Throw_If_Short_Name_Is_Invalid(string value)
+ {
+ // Given, When
+ var option = Record.Exception(() => new CommandOptionAttribute(value));
+
+ // Then
+ option.ShouldBeOfType().And(e =>
+ e.Message.ShouldBe("Short option names can not be longer than one character."));
+ }
+
+ [Theory]
+ [InlineData("--o")]
+ public void Should_Throw_If_Long_Name_Is_Invalid(string value)
+ {
+ // Given, When
+ var option = Record.Exception(() => new CommandOptionAttribute(value));
+
+ // Then
+ option.ShouldBeOfType().And(e =>
+ e.Message.ShouldBe("Long option names must consist of more than one character."));
+ }
+
+ [Theory]
+ [InlineData("-")]
+ [InlineData("--")]
+ public void Should_Throw_If_Option_Have_No_Name(string template)
+ {
+ // Given, When
+ var option = Record.Exception(() => new CommandOptionAttribute(template));
+
+ // Then
+ option.ShouldBeOfType().And(e =>
+ e.Message.ShouldBe("Options without name are not allowed."));
+ }
+
+ [Theory]
+ [InlineData("--foo|-foo[b", '[')]
+ [InlineData("--foo|-f€b", '€')]
+ [InlineData("--foo|-foo@b", '@')]
+ public void Should_Throw_If_Option_Contains_Invalid_Name(string template, char invalid)
+ {
+ // Given, When
+ var result = Record.Exception(() => new CommandOptionAttribute(template));
+
+ // Then
+ result.ShouldBeOfType().And(e =>
+ {
+ e.Message.ShouldBe($"Encountered invalid character '{invalid}' in option name.");
+ e.Template.ShouldBe(template);
+ });
+ }
+
+ [Theory]
+ [InlineData("--foo ", "HELLO-WORLD")]
+ [InlineData("--foo ", "HELLO_WORLD")]
+ public void Should_Accept_Dash_And_Underscore_In_Value_Name(string template, string name)
+ {
+ // Given, When
+ var result = new CommandOptionAttribute(template);
+
+ // Then
+ result.ValueName.ShouldBe(name);
+ }
+
+ [Theory]
+ [InlineData("--foo|-1")]
+ public void Should_Throw_If_First_Letter_Of_An_Option_Name_Is_A_Digit(string template)
+ {
+ // Given, When
+ var result = Record.Exception(() => new CommandOptionAttribute(template));
+
+ // Then
+ result.ShouldBeOfType().And(e =>
+ {
+ e.Message.ShouldBe("Option names cannot start with a digit.");
+ e.Template.ShouldBe(template);
+ });
+ }
+
+ [Fact]
+ public void Multiple_Short_Options_Are_Supported()
+ {
+ // Given, When
+ var result = new CommandOptionAttribute("-f|-b");
+
+ // Then
+ result.ShortNames.Count.ShouldBe(2);
+ result.ShortNames.ShouldContain("f");
+ result.ShortNames.ShouldContain("b");
+ }
+
+ [Fact]
+ public void Multiple_Long_Options_Are_Supported()
+ {
+ // Given, When
+ var result = new CommandOptionAttribute("--foo|--bar");
+
+ // Then
+ result.LongNames.Count.ShouldBe(2);
+ result.LongNames.ShouldContain("foo");
+ result.LongNames.ShouldContain("bar");
+ }
+
+ [Theory]
+ [InlineData("-f|--foo ")]
+ [InlineData("--foo|-f ")]
+ [InlineData(" --foo|-f")]
+ [InlineData(" -f|--foo")]
+ [InlineData("-f --foo")]
+ [InlineData("--foo -f")]
+ public void Template_Parts_Can_Appear_In_Any_Order(string template)
+ {
+ // Given, When
+ var result = new CommandOptionAttribute(template);
+
+ // Then
+ result.LongNames.ShouldContain("foo");
+ result.ShortNames.ShouldContain("f");
+ result.ValueName.ShouldBe("BAR");
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs
new file mode 100644
index 0000000..9e7483d
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs
@@ -0,0 +1,234 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Shouldly;
+using Spectre.Console.Cli;
+using Spectre.Console.Tests.Data;
+using Spectre.Console.Testing;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class FlagValues
+ {
+ [SuppressMessage("Performance", "CA1812", Justification = "It's OK")]
+ private sealed class FlagSettings : CommandSettings
+ {
+ [CommandOption("--serve [PORT]")]
+ public FlagValue Serve { get; set; }
+ }
+
+ [SuppressMessage("Performance", "CA1812", Justification = "It's OK")]
+ private sealed class FlagSettingsWithNullableValueType : CommandSettings
+ {
+ [CommandOption("--serve [PORT]")]
+ public FlagValue Serve { get; set; }
+ }
+
+ [SuppressMessage("Performance", "CA1812", Justification = "It's OK")]
+ private sealed class FlagSettingsWithOptionalOptionButNoFlagValue : CommandSettings
+ {
+ [CommandOption("--serve [PORT]")]
+ public int Serve { get; set; }
+ }
+
+ [SuppressMessage("Performance", "CA1812", Justification = "It's OK")]
+ private sealed class FlagSettingsWithDefaultValue : CommandSettings
+ {
+ [CommandOption("--serve [PORT]")]
+ [DefaultValue(987)]
+ public FlagValue Serve { get; set; }
+ }
+
+ [Fact]
+ public void Should_Throw_If_Command_Option_Value_Is_Optional_But_Type_Is_Not_A_Flag_Value()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "foo", "--serve", "123" }));
+
+ // Then
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("The option 'serve' has an optional value but does not implement IFlagValue.");
+ });
+ }
+
+ [Fact]
+ public void Should_Set_Flag_And_Value_If_Both_Were_Provided()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "foo", "--serve", "123",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(flag =>
+ {
+ flag.Serve.IsSet.ShouldBeTrue();
+ flag.Serve.Value.ShouldBe(123);
+ });
+ }
+
+ [Fact]
+ public void Should_Only_Set_Flag_If_No_Value_Was_Provided()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "foo", "--serve",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(flag =>
+ {
+ flag.Serve.IsSet.ShouldBeTrue();
+ flag.Serve.Value.ShouldBe(0);
+ });
+ }
+
+ [Fact]
+ public void Should_Set_Value_To_Default_Value_If_None_Was_Explicitly_Set()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "foo", "--serve",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(flag =>
+ {
+ flag.Serve.IsSet.ShouldBeTrue();
+ flag.Serve.Value.ShouldBe(987);
+ });
+ }
+
+ [Fact]
+ public void Should_Create_Unset_Instance_If_Flag_Was_Not_Set()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "foo",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(flag =>
+ {
+ flag.Serve.IsSet.ShouldBeFalse();
+ flag.Serve.Value.ShouldBe(0);
+ });
+ }
+
+ [Fact]
+ public void Should_Create_Unset_Instance_With_Null_For_Nullable_Value_Type_If_Flag_Was_Not_Set()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("foo");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "foo",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(flag =>
+ {
+ flag.Serve.IsSet.ShouldBeFalse();
+ flag.Serve.Value.ShouldBeNull();
+ });
+ }
+
+ [Theory]
+ [InlineData("Foo", true, "Set=True, Value=Foo")]
+ [InlineData("Bar", false, "Set=False, Value=Bar")]
+ public void Should_Return_Correct_String_Representation_From_ToString(
+ string value,
+ bool isSet,
+ string expected)
+ {
+ // Given
+ var flag = new FlagValue();
+ flag.Value = value;
+ flag.IsSet = isSet;
+
+ // When
+ var result = flag.ToString();
+
+ // Then
+ result.ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData(true, "Set=True")]
+ [InlineData(false, "Set=False")]
+ public void Should_Return_Correct_String_Representation_From_ToString_If_Value_Is_Not_Set(
+ bool isSet,
+ string expected)
+ {
+ // Given
+ var flag = new FlagValue();
+ flag.IsSet = isSet;
+
+ // When
+ var result = flag.ToString();
+
+ // Then
+ result.ShouldBe(expected);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs
new file mode 100644
index 0000000..2d7c763
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs
@@ -0,0 +1,231 @@
+using System.Threading.Tasks;
+using Spectre.Console.Cli;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using VerifyXunit;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ [UsesVerify]
+ public class Help
+ {
+ [Fact]
+ public Task Should_Output_Root_Correctly()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddCommand("dog");
+ configurator.AddCommand("horse");
+ configurator.AddCommand("giraffe");
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Skip_Hidden_Commands()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddCommand("dog");
+ configurator.AddCommand("horse");
+ configurator.AddCommand("giraffe").IsHidden();
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Command_Correctly()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddBranch("cat", animal =>
+ {
+ animal.SetDescription("Contains settings for a cat.");
+ animal.AddCommand("lion");
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("cat", "--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Leaf_Correctly()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddBranch("cat", animal =>
+ {
+ animal.SetDescription("Contains settings for a cat.");
+ animal.AddCommand("lion");
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("cat", "lion", "--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Default_Command_Correctly()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.WithDefaultCommand();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Root_Examples_Defined_On_Root()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" });
+ configurator.AddExample(new[] { "horse", "--name", "Brutus" });
+ configurator.AddCommand("dog");
+ configurator.AddCommand("horse");
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddCommand("dog")
+ .WithExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" });
+ configurator.AddCommand("horse")
+ .WithExample(new[] { "horse", "--name", "Brutus" });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddBranch("animal", animal =>
+ {
+ animal.SetDescription("The animal command.");
+ animal.AddCommand("dog")
+ .WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" });
+ animal.AddCommand("horse")
+ .WithExample(new[] { "animal", "horse", "--name", "Brutus" });
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Only_Output_Command_Examples_Defined_On_Command()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddBranch("animal", animal =>
+ {
+ animal.SetDescription("The animal command.");
+ animal.AddExample(new[] { "animal", "--help" });
+
+ animal.AddCommand("dog")
+ .WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" });
+ animal.AddCommand("horse")
+ .WithExample(new[] { "animal", "horse", "--name", "Brutus" });
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("animal", "--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Output_Root_Examples_If_Default_Command_Is_Specified()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.WithDefaultCommand();
+ fixture.Configure(configurator =>
+ {
+ configurator.SetApplicationName("myapp");
+ configurator.AddExample(new[] { "12", "-c", "3" });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run("--help");
+
+ // Then
+ return Verifier.Verify(output);
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs
new file mode 100644
index 0000000..77a56c2
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs
@@ -0,0 +1,67 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class Injection
+ {
+ public sealed class FakeDependency
+ {
+ }
+
+ public sealed class InjectSettings : CommandSettings
+ {
+ public FakeDependency Fake { get; set; }
+
+ [CommandOption("--name ")]
+ public string Name { get; }
+
+ [CommandOption("--age ")]
+ public int Age { get; set; }
+
+ public InjectSettings(FakeDependency fake, string name)
+ {
+ Fake = fake;
+ Name = "Hello " + name;
+ }
+ }
+
+ [Fact]
+ public void Should_Inject_Parameters()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ var dependency = new FakeDependency();
+
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.Settings.Registrar.RegisterInstance(dependency);
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--name", "foo",
+ "--age", "35",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(injected =>
+ {
+ injected.ShouldNotBeNull();
+ injected.Fake.ShouldBeSameAs(dependency);
+ injected.Name.ShouldBe("Hello foo");
+ injected.Age.ShouldBe(35);
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs
new file mode 100644
index 0000000..3e8cb64
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs
@@ -0,0 +1,292 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class Pairs
+ {
+ public sealed class AmbiguousSettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ [PairDeconstructor(typeof(StringIntDeconstructor))]
+ [TypeConverter(typeof(CatAgilityConverter))]
+ public ILookup Values { get; set; }
+ }
+
+ public sealed class NotDeconstructableSettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ [PairDeconstructor(typeof(StringIntDeconstructor))]
+ public string Values { get; set; }
+ }
+
+ public sealed class DefaultPairDeconstructorSettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ public IDictionary Values { get; set; }
+ }
+
+ public sealed class LookupSettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ [PairDeconstructor(typeof(StringIntDeconstructor))]
+ public ILookup Values { get; set; }
+ }
+
+ public sealed class DictionarySettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ [PairDeconstructor(typeof(StringIntDeconstructor))]
+ public IDictionary Values { get; set; }
+ }
+
+ public sealed class ReadOnlyDictionarySettings : CommandSettings
+ {
+ [CommandOption("--var ")]
+ [PairDeconstructor(typeof(StringIntDeconstructor))]
+ public IReadOnlyDictionary Values { get; set; }
+ }
+
+ public sealed class StringIntDeconstructor : PairDeconstuctor
+ {
+ protected override (string Key, string Value) Deconstruct(string value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ var parts = value.Split(new[] { '=' });
+ if (parts.Length != 2)
+ {
+ throw new InvalidOperationException("Could not parse pair");
+ }
+
+ return (parts[0], parts[1]);
+ }
+ }
+
+ [Fact]
+ public void Should_Throw_If_Option_Has_Pair_Deconstructor_And_Type_Converter()
+ {
+ // Given
+ var app = new CommandApp>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "foo=qux",
+ }));
+
+ // Then
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("The option 'var' is both marked as pair deconstructable and convertable.");
+ });
+ }
+
+ [Fact]
+ public void Should_Throw_If_Option_Has_Pair_Deconstructor_But_Type_Is_Not_Deconstructable()
+ {
+ // Given
+ var app = new CommandApp>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "foo=qux",
+ }));
+
+ // Then
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("The option 'var' is marked as pair deconstructable, but the underlying type does not support that.");
+ });
+ }
+
+ [Fact]
+ public void Should_Map_Pairs_To_Pair_Deconstructable_Collection_Using_Default_Deconstructort()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--var", "foo=1",
+ "--var", "foo=3",
+ "--var", "bar=4",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(pair =>
+ {
+ pair.Values.ShouldNotBeNull();
+ pair.Values.Count.ShouldBe(2);
+ pair.Values["foo"].ShouldBe(3);
+ pair.Values["bar"].ShouldBe(4);
+ });
+ }
+
+ [Theory]
+ [InlineData("foo=1=2", "Error: The value 'foo=1=2' is not in a correct format")]
+ [InlineData("foo=1=2=3", "Error: The value 'foo=1=2=3' is not in a correct format")]
+ public void Should_Throw_If_Value_Is_Not_In_A_Valid_Format_Using_Default_Deconstructor(
+ string input, string expected)
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+
+ // When
+ var (result, output, _, settings) = app.Run(new[]
+ {
+ "--var", input,
+ });
+
+ // Then
+ result.ShouldBe(-1);
+ output.ShouldBe(expected);
+ }
+
+ [Fact]
+ public void Should_Map_Lookup_Values()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "foo=qux",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(pair =>
+ {
+ pair.Values.ShouldNotBeNull();
+ pair.Values.Count.ShouldBe(1);
+ pair.Values["foo"].ToList().Count.ShouldBe(2);
+ });
+ }
+
+ [Fact]
+ public void Should_Map_Dictionary_Values()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "baz=qux",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(pair =>
+ {
+ pair.Values.ShouldNotBeNull();
+ pair.Values.Count.ShouldBe(2);
+ pair.Values["foo"].ShouldBe("bar");
+ pair.Values["baz"].ShouldBe("qux");
+ });
+ }
+
+ [Fact]
+ public void Should_Map_Latest_Value_Of_Same_Key_When_Mapping_To_Dictionary()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "foo=qux",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(pair =>
+ {
+ pair.Values.ShouldNotBeNull();
+ pair.Values.Count.ShouldBe(1);
+ pair.Values["foo"].ShouldBe("qux");
+ });
+ }
+
+ [Fact]
+ public void Should_Map_ReadOnly_Dictionary_Values()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.WithDefaultCommand>();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "--var", "foo=bar",
+ "--var", "baz=qux",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(pair =>
+ {
+ pair.Values.ShouldNotBeNull();
+ pair.Values.Count.ShouldBe(2);
+ pair.Values["foo"].ShouldBe("bar");
+ pair.Values["baz"].ShouldBe("qux");
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs
new file mode 100644
index 0000000..8820ff5
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs
@@ -0,0 +1,615 @@
+using System;
+using System.Threading.Tasks;
+using Shouldly;
+using Spectre.Console.Cli;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using VerifyXunit;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public static class Parsing
+ {
+ [UsesVerify]
+ public sealed class UnknownCommand
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_When_Command_Is_Unknown()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(config =>
+ {
+ config.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("cat", "14");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(config =>
+ {
+ config.AddCommand("cat");
+ });
+
+ // When
+ var result = fixture.Run("bat", "14");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(config =>
+ {
+ config.AddBranch("dog", a =>
+ {
+ a.AddCommand("cat");
+ });
+ });
+
+ // When
+ var result = fixture.Run("dog", "bat", "14");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.WithDefaultCommand>();
+ fixture.Configure(config =>
+ {
+ config.AddCommand>("cat");
+ });
+
+ // When
+ var result = fixture.Run("bat");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.WithDefaultCommand>();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddBranch("dog", a =>
+ {
+ a.AddCommand("cat");
+ });
+ });
+
+ // When
+ var result = fixture.Run("dog", "bat");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.WithDefaultCommand>();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand>("bar");
+ });
+
+ // When
+ var result = fixture.Run("qux", "bat");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddBranch("foo", a =>
+ {
+ a.AddCommand>("bar");
+ });
+ });
+
+ // When
+ var result = fixture.Run("foo", "qux", "bat");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("empty");
+ });
+
+ // When
+ var result = fixture.Run("empty", "other");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class CannotAssignValueToFlag
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Long_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "--alive", "foo");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Short_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "-a", "foo");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class NoValueForOption
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Long_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "--name");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Short_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "-n");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class NoMatchingArgument
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("giraffe");
+ });
+
+ // When
+ var result = fixture.Run("giraffe", "foo", "bar", "baz");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class UnexpectedOption
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Long_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("--foo");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Short_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("-f");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class UnknownOption
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.UseStrictParsing();
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "--unknown");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.UseStrictParsing();
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "-u");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class UnterminatedQuote
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("--name", "\"Rufus");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class OptionWithoutName
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Short_Option()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", "-", " ");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"--foo=");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"--foo:");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"-f=");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Fact]
+ public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"-f:");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class InvalidShortOptionName
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"-f0o");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class LongOptionNameIsOneCharacter
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"--f");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class LongOptionNameIsMissing
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"-- ");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class LongOptionNameStartWithDigit
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"--1foo");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+ }
+
+ [UsesVerify]
+ public sealed class LongOptionNameContainSymbol
+ {
+ [Fact]
+ public Task Should_Return_Correct_Text()
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", $"--f€oo");
+
+ // Then
+ return Verifier.Verify(result);
+ }
+
+ [Theory]
+ [InlineData("--f-oo")]
+ [InlineData("--f-o-o")]
+ [InlineData("--f_oo")]
+ [InlineData("--f_o_o")]
+ public void Should_Allow_Special_Symbols_In_Name(string option)
+ {
+ // Given
+ var fixture = new Fixture();
+ fixture.Configure(configurator =>
+ {
+ configurator.AddCommand("dog");
+ });
+
+ // When
+ var result = fixture.Run("dog", option);
+
+ // Then
+ result.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'.");
+ }
+ }
+ }
+
+ internal sealed class Fixture
+ {
+ private Action _appConfiguration = _ => { };
+ private Action _configuration;
+
+ public void WithDefaultCommand()
+ where T : class, ICommand
+ {
+ _appConfiguration = (app) => app.SetDefaultCommand();
+ }
+
+ public void Configure(Action action)
+ {
+ _configuration = action;
+ }
+
+ public string Run(params string[] args)
+ {
+ using (var console = new FakeConsole())
+ {
+ var app = new CommandApp();
+ _appConfiguration?.Invoke(app);
+
+ app.Configure(_configuration);
+ app.Configure(c => c.ConfigureConsole(console));
+ app.Run(args);
+
+ return console.Output
+ .NormalizeLineEndings()
+ .TrimLines()
+ .Trim();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs
new file mode 100644
index 0000000..d82150e
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs
@@ -0,0 +1,118 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandApptests
+ {
+ [Fact]
+ public void Should_Treat_Commands_As_Case_Sensitive_If_Specified()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.UseStrictParsing();
+ config.PropagateExceptions();
+ config.CaseSensitivity(CaseSensitivity.Commands);
+ config.AddCommand>("command");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[]
+ {
+ "Command", "--foo", "bar",
+ }));
+
+ // Then
+ result.ShouldNotBeNull();
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("Unknown command 'Command'.");
+ });
+ }
+
+ [Fact]
+ public void Should_Treat_Long_Options_As_Case_Sensitive_If_Specified()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.UseStrictParsing();
+ config.PropagateExceptions();
+ config.CaseSensitivity(CaseSensitivity.LongOptions);
+ config.AddCommand>("command");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[]
+ {
+ "command", "--Foo", "bar",
+ }));
+
+ // Then
+ result.ShouldNotBeNull();
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("Unknown option 'Foo'.");
+ });
+ }
+
+ [Fact]
+ public void Should_Treat_Short_Options_As_Case_Sensitive()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.UseStrictParsing();
+ config.PropagateExceptions();
+ config.AddCommand>("command");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[]
+ {
+ "command", "-F", "bar",
+ }));
+
+ // Then
+ result.ShouldNotBeNull();
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("Unknown option 'F'.");
+ });
+ }
+
+ [Fact]
+ public void Should_Suppress_Case_Sensitivity_If_Specified()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.UseStrictParsing();
+ config.PropagateExceptions();
+ config.CaseSensitivity(CaseSensitivity.None);
+ config.AddCommand>("command");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "Command", "--Foo", "bar",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(vec =>
+ {
+ vec.Foo.ShouldBe("bar");
+ });
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs
new file mode 100644
index 0000000..a794807
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs
@@ -0,0 +1,26 @@
+using Shouldly;
+using Spectre.Console.Cli;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ [Fact]
+ public void Should_Apply_Case_Sensitivity_For_Everything_By_Default()
+ {
+ // Given
+ var app = new CommandApp();
+
+ // When
+ var defaultSensitivity = CaseSensitivity.None;
+ app.Configure(config =>
+ {
+ defaultSensitivity = config.Settings.CaseSensitivity;
+ });
+
+ // Then
+ defaultSensitivity.ShouldBe(CaseSensitivity.All);
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs
new file mode 100644
index 0000000..820f84c
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs
@@ -0,0 +1,40 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class TypeConverters
+ {
+ [Fact]
+ public void Should_Bind_Using_Custom_Type_Converter_If_Specified()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand("cat");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "cat", "--name", "Tiger",
+ "--agility", "FOOBAR",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(cat =>
+ {
+ cat.Agility.ShouldBe(6);
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs
new file mode 100644
index 0000000..75460a8
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs
@@ -0,0 +1,299 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Spectre.Console.Cli.Unsafe;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class SafetyOff
+ {
+ [Fact]
+ public void Can_Mix_Safe_And_Unsafe_Configurators()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+
+ config.AddBranch("animal", animal =>
+ {
+ animal.SafetyOff().AddBranch("mammal", typeof(MammalSettings), mammal =>
+ {
+ mammal.AddCommand("dog", typeof(DogCommand));
+ mammal.AddCommand("horse", typeof(HorseCommand));
+ });
+ });
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "animal", "--alive", "mammal", "--name",
+ "Rufus", "dog", "12", "--good-boy",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(dog =>
+ {
+ dog.Age.ShouldBe(12);
+ dog.GoodBoy.ShouldBe(true);
+ dog.Name.ShouldBe("Rufus");
+ dog.IsAlive.ShouldBe(true);
+ });
+ }
+
+ [Fact]
+ public void Can_Turn_Safety_On_After_Turning_It_Off_For_Branch()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+
+ config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal =>
+ {
+ animal.SafetyOn()
+ .AddBranch("mammal", mammal =>
+ {
+ mammal.SafetyOff().AddCommand("dog", typeof(DogCommand));
+ mammal.AddCommand("horse");
+ });
+ });
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "animal", "--alive", "mammal", "--name",
+ "Rufus", "dog", "12", "--good-boy",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(dog =>
+ {
+ dog.Age.ShouldBe(12);
+ dog.GoodBoy.ShouldBe(true);
+ dog.Name.ShouldBe("Rufus");
+ dog.IsAlive.ShouldBe(true);
+ });
+ }
+
+ [Fact]
+ public void Should_Throw_If_Trying_To_Convert_Unsafe_Branch_Configurator_To_Safe_Version_With_Wrong_Type()
+ {
+ // Given
+ var app = new CommandApp();
+
+ // When
+ var result = Record.Exception(() => app.Configure(config =>
+ {
+ config.PropagateExceptions();
+
+ config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal =>
+ {
+ animal.SafetyOn().AddCommand("dog");
+ });
+ }));
+
+ // Then
+ result.ShouldBeOfType();
+ result.Message.ShouldBe("Configurator cannot be converted to a safe configurator of type 'MammalSettings'.");
+ }
+
+ [Fact]
+ public void Should_Pass_Case_1()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+
+ config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal =>
+ {
+ animal.AddBranch("mammal", typeof(MammalSettings), mammal =>
+ {
+ mammal.AddCommand("dog", typeof(DogCommand));
+ mammal.AddCommand("horse", typeof(HorseCommand));
+ });
+ });
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "animal", "--alive", "mammal", "--name",
+ "Rufus", "dog", "12", "--good-boy",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().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 CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.SafetyOff().AddCommand("dog", typeof(DogCommand));
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "dog", "12", "4", "--good-boy",
+ "--name", "Rufus", "--alive",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(dog =>
+ {
+ dog.Legs.ShouldBe(12);
+ dog.Age.ShouldBe(4);
+ dog.GoodBoy.ShouldBe(true);
+ dog.Name.ShouldBe("Rufus");
+ dog.IsAlive.ShouldBe(true);
+ });
+ }
+
+ [Fact]
+ public void Should_Pass_Case_3()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal =>
+ {
+ animal.AddCommand("dog", typeof(DogCommand));
+ animal.AddCommand("horse", typeof(HorseCommand));
+ });
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "animal", "dog", "12", "--good-boy",
+ "--name", "Rufus",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(dog =>
+ {
+ dog.Age.ShouldBe(12);
+ dog.GoodBoy.ShouldBe(true);
+ dog.Name.ShouldBe("Rufus");
+ dog.IsAlive.ShouldBe(false);
+ });
+ }
+
+ [Fact]
+ public void Should_Pass_Case_4()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal =>
+ {
+ animal.AddCommand("dog", typeof(DogCommand));
+ });
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "animal", "4", "dog", "12",
+ "--good-boy", "--name", "Rufus",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(dog =>
+ {
+ dog.Legs.ShouldBe(4);
+ dog.Age.ShouldBe(12);
+ dog.GoodBoy.ShouldBe(true);
+ dog.IsAlive.ShouldBe(false);
+ dog.Name.ShouldBe("Rufus");
+ });
+ }
+
+ [Fact]
+ public void Should_Pass_Case_5()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.SafetyOff().AddCommand("multi", typeof(OptionVectorCommand));
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "multi", "--foo", "a", "--foo", "b", "--bar", "1", "--foo", "c", "--bar", "2",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(vec =>
+ {
+ vec.Foo.Length.ShouldBe(3);
+ vec.Foo.ShouldBe(new[] { "a", "b", "c" });
+ vec.Bar.Length.ShouldBe(2);
+ vec.Bar.ShouldBe(new[] { 1, 2 });
+ });
+ }
+
+ [Fact]
+ public void Should_Pass_Case_6()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("multi");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "multi", "a", "b", "c",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(vec =>
+ {
+ vec.Foo.Length.ShouldBe(3);
+ vec.Foo.ShouldBe(new[] { "a", "b", "c" });
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs
new file mode 100644
index 0000000..97c7607
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs
@@ -0,0 +1,88 @@
+using Shouldly;
+using Spectre.Console.Cli;
+using Spectre.Console.Tests.Data;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class Validation
+ {
+ [Fact]
+ public void Should_Throw_If_Attribute_Validation_Fails()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddCommand("dog");
+ animal.AddCommand("horse");
+ });
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "animal", "3", "dog", "7", "--name", "Rufus" }));
+
+ // Then
+ result.ShouldBeOfType().And(e =>
+ {
+ e.Message.ShouldBe("Animals must have an even number of legs.");
+ });
+ }
+
+ [Fact]
+ public void Should_Throw_If_Settings_Validation_Fails()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddCommand("dog");
+ animal.AddCommand("horse");
+ });
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "animal", "4", "dog", "7", "--name", "Tiger" }));
+
+ // Then
+ result.ShouldBeOfType().And(e =>
+ {
+ e.Message.ShouldBe("Tiger is not a dog name!");
+ });
+ }
+
+ [Fact]
+ public void Should_Throw_If_Command_Validation_Fails()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddCommand("dog");
+ animal.AddCommand("horse");
+ });
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "animal", "4", "dog", "101", "--name", "Rufus" }));
+
+ // Then
+ result.ShouldBeOfType().And(e =>
+ {
+ e.Message.ShouldBe("Dog is too old...");
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs
new file mode 100644
index 0000000..fe60da5
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs
@@ -0,0 +1,111 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class Vectors
+ {
+ [Fact]
+ public void Should_Throw_If_A_Single_Command_Has_Multiple_Argument_Vectors()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("multi");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "multi", "a", "b", "c" }));
+
+ // Then
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("The command 'multi' specifies more than one vector argument.");
+ });
+ }
+
+ [Fact]
+ public void Should_Throw_If_An_Argument_Vector_Is_Not_Specified_Last()
+ {
+ // Given
+ var app = new CommandApp();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("multi");
+ });
+
+ // When
+ var result = Record.Exception(() => app.Run(new[] { "multi", "a", "b", "c" }));
+
+ // Then
+ result.ShouldBeOfType().And(ex =>
+ {
+ ex.Message.ShouldBe("The command 'multi' specifies an argument vector that is not the last argument.");
+ });
+ }
+
+ [Fact]
+ public void Should_Assign_Values_To_Argument_Vector()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand>("multi");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "multi", "a", "b", "c",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(vec =>
+ {
+ vec.Foo.Length.ShouldBe(3);
+ vec.Foo[0].ShouldBe("a");
+ vec.Foo[1].ShouldBe("b");
+ vec.Foo[2].ShouldBe("c");
+ });
+ }
+
+ [Fact]
+ public void Should_Assign_Values_To_Option_Vector()
+ {
+ // Given
+ var app = new CommandAppFixture();
+ app.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddCommand("cmd");
+ });
+
+ // When
+ var (result, _, _, settings) = app.Run(new[]
+ {
+ "cmd", "--foo", "red",
+ "--bar", "4", "--foo", "blue",
+ });
+
+ // Then
+ result.ShouldBe(0);
+ settings.ShouldBeOfType().And(vec =>
+ {
+ vec.Foo.ShouldBe(new string[] { "red", "blue" });
+ vec.Bar.ShouldBe(new int[] { 4 });
+ });
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs
new file mode 100644
index 0000000..324d9e2
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs
@@ -0,0 +1,37 @@
+using Shouldly;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ public sealed class Version
+ {
+ [Fact]
+ public void Should_Output_The_Version_To_The_Console()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddBranch("mammal", mammal =>
+ {
+ mammal.AddCommand("dog");
+ mammal.AddCommand("horse");
+ });
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.VersionCommand);
+
+ // Then
+ output.ShouldStartWith("Spectre.Cli version ");
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs
new file mode 100644
index 0000000..321f172
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs
@@ -0,0 +1,148 @@
+using System.Threading.Tasks;
+using Spectre.Console.Testing;
+using Spectre.Console.Tests.Data;
+using VerifyXunit;
+using Xunit;
+using Spectre.Console.Cli;
+
+namespace Spectre.Console.Tests.Unit.Cli
+{
+ public sealed partial class CommandAppTests
+ {
+ [UsesVerify]
+ public sealed class Xml
+ {
+ ///
+ /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-1
+ ///
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Case_1()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.PropagateExceptions();
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddBranch("mammal", mammal =>
+ {
+ mammal.AddCommand("dog");
+ mammal.AddCommand("horse");
+ });
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand);
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ ///
+ /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-2
+ ///
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Case_2()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.AddCommand("dog");
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand);
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ ///
+ /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-3
+ ///
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Case_3()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddCommand("dog");
+ animal.AddCommand("horse");
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand);
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ ///
+ /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-4
+ ///
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Case_4()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.AddBranch("animal", animal =>
+ {
+ animal.AddCommand("dog");
+ });
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand);
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ ///
+ /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-5
+ ///
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Case_5()
+ {
+ // Given
+ var fixture = new CommandAppFixture();
+ fixture.Configure(config =>
+ {
+ config.AddCommand("cmd");
+ });
+
+ // When
+ var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand);
+
+ // Then
+ return Verifier.Verify(output);
+ }
+
+ [Fact]
+ public Task Should_Dump_Correct_Model_For_Model_With_Default_Command()
+ {
+ // Given
+ var fixture = new CommandAppFixture().WithDefaultCommand();
+ fixture.Configure(config =>
+ {
+ config.AddCommand