Compare commits

...

5 Commits

Author SHA1 Message Date
Mattias Karlsson
2d9e8069fd Fix OPENCLI_VISIBILITY_INTERNAL to DefineConstants concat 2025-10-06 23:19:10 +02:00
Patrik Svensson
b551bbd244 Add OpenCLI integration to Spectre.Console.Cli 2025-10-02 02:05:46 +02:00
Patrik Svensson
3a70fbec75 Update the blog post with info about 0.51.1 2025-09-07 01:04:05 +02:00
Patrik Svensson
c67b3df3ba Fix package references 2025-09-07 00:44:00 +02:00
Patrik Svensson
8e474f514c Add blog post for 0.51 release 2025-09-07 00:29:44 +02:00
22 changed files with 601 additions and 37 deletions

View File

@@ -0,0 +1,43 @@
Title: Spectre.Console 0.51.1 released!
Description: Not a substitute for human interaction.
Published: 2025-09-07
Category: Release Notes
Excluded: false
---
Version `0.51.1` of Spectre.Console has been released!
_Note: Due to an issue discovered after the release of version 0.51.0, that version has now been unlisted. Lets all pretend it never existed 😅_
## What's Changed
* Fix IndexOutOfRangeException in ExceptionFormatter by [@martincostello](https://github.com/martincostello) in [#1800](https://github.com/spectreconsole/spectre.console/pull/1800)
* TestConsole can now be configured and accessed in CommandAppTester by [@magiino](https://github.com/magiino) in [#1803](https://github.com/spectreconsole/spectre.console/pull/1803)
* Add ShowRowSeparators in Table Widget docs by [@bartoginski](https://github.com/bartoginski) in [#1807](https://github.com/spectreconsole/spectre.console/pull/1807)
* Add support for required options by [@patriksvensson](https://github.com/patriksvensson) in [#1825](https://github.com/spectreconsole/spectre.console/pull/1825)
* Added documentation for align widget by [@Elementttto](https://github.com/Elementttto) in [#1746](https://github.com/spectreconsole/spectre.console/pull/1746)
* Fixed link not displayed in markup in Style.cs and added unit test cases by [@Elementttto](https://github.com/Elementttto) in [#1750](https://github.com/spectreconsole/spectre.console/pull/1750)
* Update System.Memory dependency by [@WeihanLi](https://github.com/WeihanLi) in [#1832](https://github.com/spectreconsole/spectre.console/pull/1832)
* Reduce memory usage for rune width cache. by [@Pannoniae](https://github.com/Pannoniae) in [#1756](https://github.com/spectreconsole/spectre.console/pull/1756)
* Fix resizing of Live views with reduced size. by [@belucha](https://github.com/belucha) in [#1840](https://github.com/spectreconsole/spectre.console/pull/1840)
* Corrects comment for optional text prompt by [@aljanabim](https://github.com/aljanabim) in [#1857](https://github.com/spectreconsole/spectre.console/pull/1857)
* Update spinners by [@FroggieFrog](https://github.com/FroggieFrog) in [#1873](https://github.com/spectreconsole/spectre.console/pull/1873)
* Support J and K for navigating list prompts by [@tobias-tengler](https://github.com/tobias-tengler) in [#1877](https://github.com/spectreconsole/spectre.console/pull/1877)
* Fix space triggering selection when items in the selection list have a space. by [@mitchdenny](https://github.com/mitchdenny) in [#1881](https://github.com/spectreconsole/spectre.console/pull/1881)
* Fix bug setting Header by [@mattfennerom](https://github.com/mattfennerom) in [#1890](https://github.com/spectreconsole/spectre.console/pull/1890)
## New Contributors
* [@magiino](https://github.com/magiino) made their first contribution in [#1803](https://github.com/spectreconsole/spectre.console/pull/1803)
* [@bartoginski](https://github.com/bartoginski) made their first contribution in [#1807](https://github.com/spectreconsole/spectre.console/pull/1807)
* [@Elementttto](https://github.com/Elementttto) made their first contribution in [#1746](https://github.com/spectreconsole/spectre.console/pull/1746)
* [@WeihanLi](https://github.com/WeihanLi) made their first contribution in [#1832](https://github.com/spectreconsole/spectre.console/pull/1832)
* [@Pannoniae](https://github.com/Pannoniae) made their first contribution in [#1756](https://github.com/spectreconsole/spectre.console/pull/1756)
* [@belucha](https://github.com/belucha) made their first contribution in [#1840](https://github.com/spectreconsole/spectre.console/pull/1840)
* [@aljanabim](https://github.com/aljanabim) made their first contribution in [#1857](https://github.com/spectreconsole/spectre.console/pull/1857)
* [@FroggieFrog](https://github.com/FroggieFrog) made their first contribution in [#1873](https://github.com/spectreconsole/spectre.console/pull/1873)
* [@tobias-tengler](https://github.com/tobias-tengler) made their first contribution in [#1877](https://github.com/spectreconsole/spectre.console/pull/1877)
* [@mitchdenny](https://github.com/mitchdenny) made their first contribution in [#1881](https://github.com/spectreconsole/spectre.console/pull/1881)
* [@mattfennerom](https://github.com/mattfennerom) made their first contribution in [#1890](https://github.com/spectreconsole/spectre.console/pull/1890)
**Full Changelog**: [0.50.0...0.51.0](https://github.com/spectreconsole/spectre.console/compare/0.50.0...0.51.1)

22
docs/input/cli/opencli.md Normal file
View File

@@ -0,0 +1,22 @@
Title: OpenCLI Integration
Order: 15
Description: OpenCLI integration
Highlights:
- Generate OpenCLI descriptions
---
From version `0.52.0` and above, you will be able to generate [OpenCLI](https://opencli.org)
descriptions from your `Spectre.Console.Cli` applications.
Simply add the `--help-dump-opencli` option to your application, and an
OpenCLI description will be written to stdout.
```shell
$ ./myapp --help-dump-opencli
```
If you want to save it to disk, pipe it to a file.
```shell
$ ./myapp --help-dump-opencli > myapp.openapi.json
```

View File

@@ -8,13 +8,11 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" /> <PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" />
<PackageVersion Include="OpenCli.Sources" Version="0.5.0" />
<PackageVersion Include="PolySharp" Version="1.15.0" /> <PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.14.0" /> <PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.14.0" />
<PackageVersion Include="Shouldly" Version="4.3.0" /> <PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="Spectre.Console" Version="0.50.0" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.50.0" />
<PackageVersion Include="Spectre.Console.Testing" Version="0.50.1-preview.0.20" />
<PackageVersion Include="Spectre.Verify.Extensions" Version="28.16.0" /> <PackageVersion Include="Spectre.Verify.Extensions" Version="28.16.0" />
<PackageVersion Include="StyleCop.Analyzers" PrivateAssets="All" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" PrivateAssets="All" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Memory" Version="4.6.3" /> <PackageVersion Include="System.Memory" Version="4.6.3" />

View File

@@ -79,6 +79,7 @@ public sealed class CommandApp : ICommandApp
cli.AddCommand<VersionCommand>(CliConstants.Commands.Version); cli.AddCommand<VersionCommand>(CliConstants.Commands.Version);
cli.AddCommand<XmlDocCommand>(CliConstants.Commands.XmlDoc); cli.AddCommand<XmlDocCommand>(CliConstants.Commands.XmlDoc);
cli.AddCommand<ExplainCommand>(CliConstants.Commands.Explain); cli.AddCommand<ExplainCommand>(CliConstants.Commands.Explain);
cli.AddCommand<OpenCliGeneratorCommand>(CliConstants.Commands.OpenCli);
}); });
_executed = true; _executed = true;

View File

@@ -68,6 +68,13 @@ internal sealed class CommandExecutor
} }
} }
} }
// OpenCLI?
if (firstArgument.Equals(CliConstants.DumpHelpOpenCliOption, StringComparison.OrdinalIgnoreCase))
{
// Replace all arguments with the opencligen command
arguments = ["cli", "opencli"];
}
} }
// Parse and map the model against the arguments. // Parse and map the model against the arguments.

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Displays diagnostics about CLI configurations")] [Description("Displays diagnostics about CLI configurations")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class ExplainCommand : Command<ExplainCommand.Settings> internal sealed class ExplainCommand : Command<ExplainCommand.Settings>, IBuiltInCommand
{ {
private readonly CommandModel _commandModel; private readonly CommandModel _commandModel;
private readonly IAnsiConsole _writer; private readonly IAnsiConsole _writer;

View File

@@ -0,0 +1,9 @@
namespace Spectre.Console.Cli;
/// <summary>
/// Represents a built-in command.
/// Used as a marker interface.
/// </summary>
internal interface IBuiltInCommand : ICommand
{
}

View File

@@ -0,0 +1,224 @@
using OpenCli;
namespace Spectre.Console.Cli;
internal sealed class OpenCliGeneratorCommand : Command, IBuiltInCommand
{
private readonly IConfiguration _configuration;
private readonly CommandModel _model;
public OpenCliGeneratorCommand(IConfiguration configuration, CommandModel model)
{
_configuration = configuration;
_model = model ?? throw new ArgumentNullException(nameof(model));
}
public override int Execute(CommandContext context)
{
var document = new OpenCliDocument
{
OpenCli = "0.1-draft",
Info = new OpenCliInfo
{
Title = ((ICommandModel)_model).ApplicationName, Version = _model.ApplicationVersion ?? "1.0",
},
Commands = CreateCommands(_model.Commands),
Arguments = CreateArguments(_model.DefaultCommand?.GetArguments()),
Options = CreateOptions(_model.DefaultCommand?.GetOptions()),
};
var writer = _configuration.Settings.Console.GetConsole();
writer.WriteLine(document.Write());
return 0;
}
private List<OpenCliCommand> CreateCommands(IList<CommandInfo> commands)
{
var result = new List<OpenCliCommand>();
foreach (var command in commands.OrderBy(o => o.Name, StringComparer.OrdinalIgnoreCase))
{
if (typeof(IBuiltInCommand).IsAssignableFrom(command.CommandType))
{
continue;
}
var openCliCommand = new OpenCliCommand
{
Name = command.Name,
Aliases =
[
..command.Aliases.OrderBy(str => str)
],
Commands = CreateCommands(command.Children),
Arguments = CreateArguments(command.GetArguments()),
Options = CreateOptions(command.GetOptions()),
Description = command.Description,
Hidden = command.IsHidden,
Examples = [..command.Examples.Select(example => string.Join(" ", example))],
};
// Skip branches without commands
if (command.IsBranch && openCliCommand.Commands.Count == 0)
{
continue;
}
result.Add(openCliCommand);
}
return result;
}
private List<OpenCliArgument> CreateArguments(IEnumerable<CommandArgument>? arguments)
{
var result = new List<OpenCliArgument>();
if (arguments == null)
{
return result;
}
foreach (var argument in arguments.OrderBy(x => x.Position))
{
var metadata = default(List<OpenCliMetadata>);
if (argument.ParameterType != typeof(void) &&
argument.ParameterType != typeof(bool))
{
metadata =
[
new OpenCliMetadata { Name = "ClrType", Value = argument.ParameterType.ToCliTypeString(), },
];
}
result.Add(new OpenCliArgument
{
Name = argument.Value,
Required = argument.IsRequired,
Arity = new OpenCliArity
{
// TODO: Look into this
Minimum = 1,
Maximum = 1,
},
Description = argument.Description,
Hidden = argument.IsHidden,
Metadata = metadata,
AcceptedValues = null,
Group = null,
});
}
return result;
}
private List<OpenCliOption> CreateOptions(IEnumerable<CommandOption>? options)
{
var result = new List<OpenCliOption>();
if (options == null)
{
return result;
}
foreach (var option in options.OrderBy(o => o.GetOptionName(), StringComparer.OrdinalIgnoreCase))
{
var arguments = new List<OpenCliArgument>();
if (option.ParameterType != typeof(void) &&
option.ParameterType != typeof(bool))
{
arguments.Add(new OpenCliArgument
{
Name = option.ValueName ?? "VALUE",
Required = !option.ValueIsOptional,
Arity = new OpenCliArity
{
// TODO: Look into this
Minimum = option.ValueIsOptional
? 0
: 1,
Maximum = 1,
},
AcceptedValues = null,
Group = null,
Hidden = null,
Metadata =
[
new OpenCliMetadata
{
Name = "ClrType",
Value = option.ParameterType.ToCliTypeString(),
},
],
});
}
var optionMetadata = default(List<OpenCliMetadata>);
if (arguments.Count == 0 && option.ParameterType != typeof(void) &&
option.ParameterType != typeof(bool))
{
optionMetadata =
[
new OpenCliMetadata { Name = "ClrType", Value = option.ParameterType.ToCliTypeString(), },
];
}
var (optionName, optionAliases) = GetOptionNames(option);
result.Add(new OpenCliOption
{
Name = optionName,
Required = option.IsRequired,
Aliases = [..optionAliases.OrderBy(str => str)],
Arguments = arguments,
Description = option.Description,
Group = null,
Hidden = option.IsHidden,
Recursive = option.IsShadowed, // TODO: Is this correct?
Metadata = optionMetadata,
});
}
return result;
}
private static (string Name, HashSet<string> Aliases) GetOptionNames(CommandOption option)
{
var name = GetOptionName(option);
var aliases = new HashSet<string>();
if (option.LongNames.Count > 0)
{
foreach (var alias in option.LongNames.Skip(1))
{
aliases.Add("--" + alias);
}
foreach (var alias in option.ShortNames)
{
aliases.Add("-" + alias);
}
}
else
{
foreach (var alias in option.LongNames)
{
aliases.Add("--" + alias);
}
foreach (var alias in option.ShortNames.Skip(1))
{
aliases.Add("-" + alias);
}
}
return (name, aliases);
}
private static string GetOptionName(CommandOption option)
{
return option.LongNames.Count > 0
? "--" + option.LongNames[0]
: "-" + option.ShortNames[0];
}
}

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Displays the CLI library version")] [Description("Displays the CLI library version")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class VersionCommand : Command<VersionCommand.Settings> internal sealed class VersionCommand : Command, IBuiltInCommand
{ {
private readonly IAnsiConsole _writer; private readonly IAnsiConsole _writer;
@@ -11,11 +11,7 @@ internal sealed class VersionCommand : Command<VersionCommand.Settings>
_writer = configuration.Settings.Console.GetConsole(); _writer = configuration.Settings.Console.GetConsole();
} }
public sealed class Settings : CommandSettings public override int Execute(CommandContext context)
{
}
public override int Execute(CommandContext context, Settings settings)
{ {
_writer.MarkupLine( _writer.MarkupLine(
"[yellow]Spectre.Cli[/] version [aqua]{0}[/]", "[yellow]Spectre.Cli[/] version [aqua]{0}[/]",

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Generates an XML representation of the CLI configuration.")] [Description("Generates an XML representation of the CLI configuration.")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings> internal sealed class XmlDocCommand : Command, IBuiltInCommand
{ {
private readonly CommandModel _model; private readonly CommandModel _model;
private readonly IAnsiConsole _writer; private readonly IAnsiConsole _writer;
@@ -13,11 +13,7 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
_writer = configuration.Settings.Console.GetConsole(); _writer = configuration.Settings.Console.GetConsole();
} }
public sealed class Settings : CommandSettings public override int Execute(CommandContext context)
{
}
public override int Execute(CommandContext context, Settings settings)
{ {
_writer.Write(Serialize(_model), Style.Plain); _writer.Write(Serialize(_model), Style.Plain);
return 0; return 0;

View File

@@ -5,6 +5,7 @@ internal static class CliConstants
public const string DefaultCommandName = "__default_command"; public const string DefaultCommandName = "__default_command";
public const string True = "true"; public const string True = "true";
public const string False = "false"; public const string False = "false";
public const string DumpHelpOpenCliOption = "--help-dump-opencli";
public static string[] AcceptedBooleanValues { get; } = new string[] public static string[] AcceptedBooleanValues { get; } = new string[]
{ {
@@ -18,5 +19,6 @@ internal static class CliConstants
public const string Version = "version"; public const string Version = "version";
public const string XmlDoc = "xmldoc"; public const string XmlDoc = "xmldoc";
public const string Explain = "explain"; public const string Explain = "explain";
public const string OpenCli = "opencli";
} }
} }

View File

@@ -0,0 +1,13 @@
#if NETSTANDARD2_0
namespace System.IO;
// Polyfills needed for OpenCli.
// This can be removed once me migrate over to the Polyfill library.
internal static class OpenCliExtensions
{
public static Task<string> ReadToEndAsync(this StreamReader reader, CancellationToken cancellationToken)
{
return reader.ReadToEndAsync();
}
}
#endif

View File

@@ -16,4 +16,19 @@ internal static class TypeExtensions
return false; return false;
} }
// Taken from https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/TypeExtensions.cs#L15
// Licensed under MIT
public static string ToCliTypeString(this Type type)
{
var typeName = type.FullName ?? string.Empty;
if (!type.IsGenericType)
{
return typeName;
}
var genericTypeName = typeName.Substring(0, typeName.IndexOf('`'));
var genericTypes = string.Join(", ", type.GenericTypeArguments.Select(generic => generic.ToCliTypeString()));
return $"{genericTypeName}<{genericTypes}>";
}
} }

View File

@@ -2,6 +2,16 @@ namespace Spectre.Console.Cli;
internal static class CommandInfoExtensions internal static class CommandInfoExtensions
{ {
public static IEnumerable<CommandArgument>? GetArguments(this CommandInfo? command)
{
return command?.Parameters.OfType<CommandArgument>();
}
public static IEnumerable<CommandOption>? GetOptions(this CommandInfo? command)
{
return command?.Parameters.OfType<CommandOption>();
}
public static bool HaveParentWithOption(this CommandInfo command, CommandOption option) public static bool HaveParentWithOption(this CommandInfo command, CommandOption option)
{ {
var parent = command?.Parent; var parent = command?.Parent;

View File

@@ -38,7 +38,7 @@ internal abstract class CommandParameter : ICommandParameterInfo, ICommandParame
DefaultValue = defaultValue; DefaultValue = defaultValue;
PairDeconstructor = deconstructor; PairDeconstructor = deconstructor;
ValueProvider = valueProvider; ValueProvider = valueProvider;
Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>()); Validators = new List<ParameterValidationAttribute>(validators ?? []);
IsRequired = required; IsRequired = required;
IsHidden = isHidden; IsHidden = isHidden;
} }

View File

@@ -9,6 +9,7 @@ global using System.IO;
global using System.Linq; global using System.Linq;
global using System.Reflection; global using System.Reflection;
global using System.Text; global using System.Text;
global using System.Threading;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using System.Xml; global using System.Xml;
global using Spectre.Console.Cli.Help; global using Spectre.Console.Cli.Help;

View File

@@ -3,11 +3,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks> <TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'">false</IsAotCompatible> <IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'">false</IsAotCompatible>
<IsTrimmable>false</IsTrimmable> <IsTrimmable>false</IsTrimmable>
<DefineConstants>$(DefineConstants);OPENCLI_VISIBILITY_INTERNAL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="REMOVE THIS"> <ItemGroup Label="REMOVE THIS">
<InternalsVisibleTo Include="Spectre.Console.Cli.Tests"/> <InternalsVisibleTo Include="Spectre.Console.Cli.Tests"/>
</ItemGroup> </ItemGroup>
@@ -18,14 +18,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Dependencies"> <ItemGroup Label="Dependencies">
<PackageReference Include="OpenCli.Sources" />
<PackageReference Include="PolySharp"> <PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Spectre.Console" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" PrivateAssets="all"/> <PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" PrivateAssets="all"/>
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/> <PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/>
@@ -46,4 +45,8 @@
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -3,16 +3,17 @@ namespace Spectre.Console.Tests;
public static class Constants public static class Constants
{ {
public static string[] VersionCommand { get; } = public static string[] VersionCommand { get; } =
new[] [
{
CliConstants.Commands.Branch, CliConstants.Commands.Branch,
CliConstants.Commands.Version, CliConstants.Commands.Version
}; ];
public static string[] XmlDocCommand { get; } = public static string[] XmlDocCommand { get; } =
new[] [
{
CliConstants.Commands.Branch, CliConstants.Commands.Branch,
CliConstants.Commands.XmlDoc, CliConstants.Commands.XmlDoc
}; ];
public static string[] OpenCliOption { get; } =
[CliConstants.DumpHelpOpenCliOption];
} }

View File

@@ -0,0 +1,190 @@
{
"opencli": "0.1-draft",
"info": {
"title": "my-app",
"version": "1.2.3"
},
"commands": [
{
"name": "animals",
"commands": [
{
"name": "cat",
"options": [
{
"name": "--agility",
"required": false,
"arguments": [
{
"name": "VALUE",
"required": true,
"arity": {
"minimum": 1,
"maximum": 1
},
"metadata": [
{
"name": "ClrType",
"value": "System.Int32"
}
]
}
],
"description": "The agility between 0 and 100.",
"recursive": false,
"hidden": false
},
{
"name": "--alive",
"required": false,
"aliases": [
"--not-dead",
"-a"
],
"description": "Indicates whether or not the animal is alive.",
"recursive": false,
"hidden": false
},
{
"name": "--name",
"required": false,
"aliases": [
"--pet-name",
"-n",
"-p"
],
"arguments": [
{
"name": "VALUE",
"required": true,
"arity": {
"minimum": 1,
"maximum": 1
},
"metadata": [
{
"name": "ClrType",
"value": "System.String"
}
]
}
],
"recursive": false,
"hidden": false
}
],
"arguments": [
{
"name": "LEGS",
"required": false,
"arity": {
"minimum": 1,
"maximum": 1
},
"description": "The number of legs.",
"hidden": false,
"metadata": [
{
"name": "ClrType",
"value": "System.Int32"
}
]
}
],
"hidden": false,
"examples": []
},
{
"name": "dog",
"options": [
{
"name": "--alive",
"required": false,
"aliases": [
"--not-dead",
"-a"
],
"description": "Indicates whether or not the animal is alive.",
"recursive": false,
"hidden": false
},
{
"name": "--good-boy",
"required": false,
"aliases": [
"-g"
],
"recursive": false,
"hidden": false
},
{
"name": "--name",
"required": false,
"aliases": [
"--pet-name",
"-n",
"-p"
],
"arguments": [
{
"name": "VALUE",
"required": true,
"arity": {
"minimum": 1,
"maximum": 1
},
"metadata": [
{
"name": "ClrType",
"value": "System.String"
}
]
}
],
"recursive": false,
"hidden": false
}
],
"arguments": [
{
"name": "LEGS",
"required": false,
"arity": {
"minimum": 1,
"maximum": 1
},
"description": "The number of legs.",
"hidden": false,
"metadata": [
{
"name": "ClrType",
"value": "System.Int32"
}
]
},
{
"name": "AGE",
"required": true,
"arity": {
"minimum": 1,
"maximum": 1
},
"hidden": false,
"metadata": [
{
"name": "ClrType",
"value": "System.Int32"
}
]
}
],
"description": "The dog command.",
"hidden": false,
"examples": []
}
],
"hidden": false,
"examples": []
}
]
}

View File

@@ -9,7 +9,6 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Shouldly" /> <PackageReference Include="Shouldly" />
<PackageReference Include="Spectre.Console.Testing" />
<PackageReference Include="Spectre.Verify.Extensions" /> <PackageReference Include="Spectre.Verify.Extensions" />
<PackageReference Include="Verify.Xunit" /> <PackageReference Include="Verify.Xunit" />
<PackageReference Include="xunit" /> <PackageReference Include="xunit" />
@@ -21,6 +20,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Spectre.Console.Cli\Spectre.Console.Cli.csproj" /> <ProjectReference Include="..\..\Spectre.Console.Cli\Spectre.Console.Cli.csproj" />
<ProjectReference Include="..\..\Spectre.Console.Testing\Spectre.Console.Testing.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,33 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
[ExpectationPath("OpenCli")]
public sealed partial class OpenCli
{
[Fact]
[Expectation("Generate")]
public Task Should_Output_OpenCli_Description()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(config =>
{
config.SetApplicationName("my-app");
config.SetApplicationVersion("1.2.3");
config.AddBranch("animals", animals =>
{
animals.AddCommand<DogCommand>("dog");
animals.AddCommand<CatCommand>("cat");
});
});
// When
var result = fixture.Run(Constants.OpenCliOption);
// Then
return Verifier.Verify(result.Output);
}
}
}