Compare commits

..

5 Commits
0.52.0 ... main

Author SHA1 Message Date
Cédric Luthi
e51812237e Update the Spectre.Console.Cli documentation with CancellationToken
Now that #1911 is merged, all `Execute` methods of commands have a new CancellationToken parameter. Update the documentation to reflect that.

New documentation shall still be written with an example on how to pass the top-level CancellationToken to the app.Run(Async) method.
2025-10-16 22:50:52 +02:00
Patrik Svensson
749f0fded8 Update Verify to 31.0.1 2025-10-11 20:54:48 +02:00
Cédric Luthi
f5f61ca610 Add top-level CancellationToken support to Spectre.Console.Cli
Also raise CA2016 (forward the CancellationToken parameter to methods that take one) to warning

Fixes #701
2025-10-11 20:51:01 +02:00
Patrik Svensson
d90e94dbb3 Fix spacing in blog post 2025-10-10 21:27:37 +02:00
Patrik Svensson
169abca986 Add blog post for 0.52.0 2025-10-10 21:12:54 +02:00
62 changed files with 248 additions and 183 deletions

View File

@@ -0,0 +1,17 @@
Title: Spectre.Console 0.52.0 released!
Description: Don't eat (too much) glue.
Published: 2025-10-10
Category: Release Notes
Excluded: false
---
Version `0.52.0` of Spectre.Console has been released!
Exciting things are happening. Weve merged support for my love child, OpenCli, in this release. That means you can now pass the parameter `--help-dump-opencli` to your application to get an [OpenCli](https://opencli.org) description dumped to stdout.
## What's Changed
* Add OpenCLI integration to Spectre.Console.Cli by [@patriksvensson](https://github.com/patriksvensson) in [#1909](https://github.com/spectreconsole/spectre.console/pull/1909)
* Fix OPENCLI_VISIBILITY_INTERNAL to DefineConstants concat by [@devlead](https://github.com/devlead) in [#1912](https://github.com/spectreconsole/spectre.console/pull/1912)
**Full Changelog**: https://github.com/spectreconsole/spectre.console/compare/0.51.1...0.52.0

View File

@@ -18,7 +18,7 @@ public class HelloCommand : Command<HelloCommand.Settings>
} }
public override int Execute(CommandContext context, Settings settings) public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
AnsiConsole.MarkupLine($"Hello, [blue]{settings.Name}[/]"); AnsiConsole.MarkupLine($"Hello, [blue]{settings.Name}[/]");
return 0; return 0;

View File

@@ -55,7 +55,7 @@ in the previous step.
```csharp ```csharp
public class AddPackageCommand : Command<AddPackageSettings> public class AddPackageCommand : Command<AddPackageSettings>
{ {
public override int Execute(CommandContext context, AddPackageSettings settings) public override int Execute(CommandContext context, AddPackageSettings settings, CancellationToken cancellationToken)
{ {
// Omitted // Omitted
return 0; return 0;
@@ -64,7 +64,7 @@ public class AddPackageCommand : Command<AddPackageSettings>
public class AddReferenceCommand : Command<AddReferenceSettings> public class AddReferenceCommand : Command<AddReferenceSettings>
{ {
public override int Execute(CommandContext context, AddReferenceSettings settings) public override int Execute(CommandContext context, AddReferenceSettings settings, CancellationToken cancellationToken)
{ {
// Omitted // Omitted
return 0; return 0;

View File

@@ -33,7 +33,7 @@ internal sealed class FileSizeCommand : Command<FileSizeCommand.Settings>
public bool IncludeHidden { get; init; } public bool IncludeHidden { get; init; }
} }
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
var searchOptions = new EnumerationOptions var searchOptions = new EnumerationOptions
{ {

View File

@@ -61,7 +61,7 @@ in the previous step.
```csharp ```csharp
public class AddPackageCommand : Command<AddPackageSettings> public class AddPackageCommand : Command<AddPackageSettings>
{ {
public override int Execute(CommandContext context, AddPackageSettings settings) public override int Execute(CommandContext context, AddPackageSettings settings, CancellationToken cancellationToken)
{ {
// Omitted // Omitted
return 0; return 0;
@@ -70,7 +70,7 @@ public class AddPackageCommand : Command<AddPackageSettings>
public class AddReferenceCommand : Command<AddReferenceSettings> public class AddReferenceCommand : Command<AddReferenceSettings>
{ {
public override int Execute(CommandContext context, AddReferenceSettings settings) public override int Execute(CommandContext context, AddReferenceSettings settings, CancellationToken cancellationToken)
{ {
// Omitted // Omitted
return 0; return 0;

View File

@@ -40,7 +40,7 @@ The following example validates the exit code and terminal output of a `Spectre.
_console = console; _console = console;
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
_console.WriteLine("Hello world."); _console.WriteLine("Hello world.");
return 0; return 0;
@@ -78,7 +78,7 @@ public sealed class InteractiveCommandTests
_console = console; _console = console;
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
var fruits = _console.Prompt( var fruits = _console.Prompt(
new MultiSelectionPrompt<string>() new MultiSelectionPrompt<string>()

View File

@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Threading;
using Generator.Models; using Generator.Models;
using Scriban; using Scriban;
using Spectre.Console.Cli; using Spectre.Console.Cli;
@@ -21,7 +22,7 @@ namespace Generator.Commands
public string Input { get; set; } public string Input { get; set; }
} }
public override int Execute(CommandContext context, Settings settings) public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
var templates = new FilePath[] var templates = new FilePath[]
{ {

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
using Generator.Models; using Generator.Models;
@@ -39,7 +40,7 @@ namespace Generator.Commands
_parser = new HtmlParser(); _parser = new HtmlParser();
} }
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings) public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
var output = new DirectoryPath(settings.Output); var output = new DirectoryPath(settings.Output);
if (!_fileSystem.Directory.Exists(settings.Output)) if (!_fileSystem.Directory.Exists(settings.Output))

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using Generator.Commands.Samples; using Generator.Commands.Samples;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Cli; using Spectre.Console.Cli;
@@ -38,7 +39,7 @@ namespace Generator.Commands
_console = new AsciiCastConsole(console); _console = new AsciiCastConsole(console);
} }
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings, CancellationToken cancellationToken)
{ {
var samples = typeof(BaseSample).Assembly var samples = typeof(BaseSample).Assembly
.GetTypes() .GetTypes()

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using Generator.Models; using Generator.Models;
using Scriban; using Scriban;
using Spectre.Console.Cli; using Spectre.Console.Cli;
@@ -16,7 +17,7 @@ namespace Generator.Commands
_fileSystem = new FileSystem(); _fileSystem = new FileSystem();
} }
public override int Execute(CommandContext context, GeneratorSettings settings) public override int Execute(CommandContext context, GeneratorSettings settings, CancellationToken cancellationToken)
{ {
// Read the spinner model. // Read the spinner model.
var spinners = new List<Spinner>(); var spinners = new List<Spinner>();

View File

@@ -100,5 +100,8 @@ dotnet_diagnostic.RCS1047.severity = none
# RCS1090: Call 'ConfigureAwait(false)'. # RCS1090: Call 'ConfigureAwait(false)'.
dotnet_diagnostic.RCS1090.severity = warning dotnet_diagnostic.RCS1090.severity = warning
# The file header is missing or not located at the top of the file # SA1633: The file header is missing or not located at the top of the file
dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1633.severity = none
# CA2016: Forward the CancellationToken parameter to methods that take one
dotnet_diagnostic.CA2016.severity = warning

View File

@@ -17,7 +17,7 @@
<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" />
<PackageVersion Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" /> <PackageVersion Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" />
<PackageVersion Include="Verify.Xunit" Version="31.0.0" /> <PackageVersion Include="Verify.Xunit" Version="31.0.1" />
<PackageVersion Include="Wcwidth.Sources" Version="3.0.0" /> <PackageVersion Include="Wcwidth.Sources" Version="3.0.0" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5">

View File

@@ -9,19 +9,20 @@ public abstract class AsyncCommand : ICommand<EmptyCommandSettings>
/// Executes the command. /// Executes the command.
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns> /// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract Task<int> ExecuteAsync(CommandContext context); public abstract Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings) Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{ {
return ExecuteAsync(context); return ExecuteAsync(context, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings) Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{ {
return ExecuteAsync(context); return ExecuteAsync(context, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -23,8 +23,9 @@ public abstract class AsyncCommand<TSettings> : ICommand<TSettings>
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns> /// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract Task<int> ExecuteAsync(CommandContext context, TSettings settings); public abstract Task<int> ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings)
@@ -33,15 +34,15 @@ public abstract class AsyncCommand<TSettings> : ICommand<TSettings>
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings) Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{ {
Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return ExecuteAsync(context, (TSettings)settings); return ExecuteAsync(context, (TSettings)settings, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings) Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken)
{ {
return ExecuteAsync(context, settings); return ExecuteAsync(context, settings, cancellationToken);
} }
} }

View File

@@ -10,19 +10,20 @@ public abstract class Command : ICommand<EmptyCommandSettings>
/// Executes the command. /// Executes the command.
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns> /// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract int Execute(CommandContext context); public abstract int Execute(CommandContext context, CancellationToken cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings) Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{ {
return Task.FromResult(Execute(context)); return Task.FromResult(Execute(context, cancellationToken));
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings) Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{ {
return Task.FromResult(Execute(context)); return Task.FromResult(Execute(context, cancellationToken));
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -26,10 +26,7 @@ public sealed class CommandApp : ICommandApp
_executor = new CommandExecutor(registrar); _executor = new CommandExecutor(registrar);
} }
/// <summary> /// <inheritdoc/>
/// Configures the command line application.
/// </summary>
/// <param name="configuration">The configuration.</param>
public void Configure(Action<IConfigurator> configuration) public void Configure(Action<IConfigurator> configuration)
{ {
if (configuration == null) if (configuration == null)
@@ -51,22 +48,14 @@ public sealed class CommandApp : ICommandApp
return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand<TCommand>()); return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand<TCommand>());
} }
/// <summary> /// <inheritdoc/>
/// Runs the command line application with specified arguments. public int Run(IEnumerable<string> args, CancellationToken cancellationToken = default)
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public int Run(IEnumerable<string> args)
{ {
return RunAsync(args).GetAwaiter().GetResult(); return RunAsync(args, cancellationToken).GetAwaiter().GetResult();
} }
/// <summary> /// <inheritdoc/>
/// Runs the command line application with specified arguments. public async Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default)
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public async Task<int> RunAsync(IEnumerable<string> args)
{ {
try try
{ {
@@ -86,7 +75,7 @@ public sealed class CommandApp : ICommandApp
} }
return await _executor return await _executor
.Execute(_configurator, args) .ExecuteAsync(_configurator, args, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
@@ -109,6 +98,11 @@ public sealed class CommandApp : ICommandApp
return _configurator.Settings.ExceptionHandler(ex, null); return _configurator.Settings.ExceptionHandler(ex, null);
} }
if (ex is OperationCanceledException)
{
return _configurator.Settings.CancellationExitCode;
}
// Render the exception. // Render the exception.
var pretty = GetRenderableErrorMessage(ex); var pretty = GetRenderableErrorMessage(ex);
if (pretty != null) if (pretty != null)

View File

@@ -25,33 +25,22 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
_defaultCommandConfigurator = _app.SetDefaultCommand<TDefaultCommand>(); _defaultCommandConfigurator = _app.SetDefaultCommand<TDefaultCommand>();
} }
/// <summary> /// <inheritdoc/>
/// Configures the command line application.
/// </summary>
/// <param name="configuration">The configuration.</param>
public void Configure(Action<IConfigurator> configuration) public void Configure(Action<IConfigurator> configuration)
{ {
_app.Configure(configuration); _app.Configure(configuration);
} }
/// <summary> /// <inheritdoc/>
/// Runs the command line application with specified arguments. public int Run(IEnumerable<string> args, CancellationToken cancellationToken = default)
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public int Run(IEnumerable<string> args)
{ {
return _app.Run(args); return _app.Run(args, cancellationToken);
} }
/// <summary> /// <inheritdoc/>
/// Runs the command line application with specified arguments. public Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default)
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public Task<int> RunAsync(IEnumerable<string> args)
{ {
return _app.RunAsync(args); return _app.RunAsync(args, cancellationToken);
} }
internal Configurator GetConfigurator() internal Configurator GetConfigurator()

View File

@@ -24,8 +24,9 @@ public abstract class Command<TSettings> : ICommand<TSettings>
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns> /// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract int Execute(CommandContext context, TSettings settings); public abstract int Execute(CommandContext context, TSettings settings, CancellationToken cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings)
@@ -34,15 +35,15 @@ public abstract class Command<TSettings> : ICommand<TSettings>
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings) Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{ {
Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return Task.FromResult(Execute(context, (TSettings)settings)); return Task.FromResult(Execute(context, (TSettings)settings, cancellationToken));
} }
/// <inheritdoc/> /// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings) Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken)
{ {
return Task.FromResult(Execute(context, settings)); return Task.FromResult(Execute(context, settings, cancellationToken));
} }
} }

View File

@@ -201,6 +201,24 @@ public static class ConfiguratorExtensions
return configurator; return configurator;
} }
/// <summary>
/// Tells the command line application to return the specified exit code when it's aborted through the <see cref="CancellationToken"/>.
/// The default cancellation exit code is 130.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="exitCode">The exit code to return in case of cancellation.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator CancellationExitCode(this IConfigurator configurator, int exitCode)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.CancellationExitCode = exitCode;
return configurator;
}
/// <summary> /// <summary>
/// Configures case sensitivity. /// Configures case sensitivity.
/// </summary> /// </summary>
@@ -304,14 +322,14 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddDelegate( public static ICommandConfigurator AddDelegate(
this IConfigurator configurator, this IConfigurator configurator,
string name, string name,
Func<CommandContext, int> func) Func<CommandContext, CancellationToken, int> func)
{ {
if (configurator == null) if (configurator == null)
{ {
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
return configurator.AddDelegate<EmptyCommandSettings>(name, (c, _) => func(c)); return configurator.AddDelegate<EmptyCommandSettings>(name, (c, _, ct) => func(c, ct));
} }
/// <summary> /// <summary>
@@ -324,14 +342,14 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddAsyncDelegate( public static ICommandConfigurator AddAsyncDelegate(
this IConfigurator configurator, this IConfigurator configurator,
string name, string name,
Func<CommandContext, Task<int>> func) Func<CommandContext, CancellationToken, Task<int>> func)
{ {
if (configurator == null) if (configurator == null)
{ {
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
return configurator.AddAsyncDelegate<EmptyCommandSettings>(name, (c, _) => func(c)); return configurator.AddAsyncDelegate<EmptyCommandSettings>(name, (c, _, ct) => func(c, ct));
} }
/// <summary> /// <summary>
@@ -345,7 +363,7 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddDelegate<TSettings>( public static ICommandConfigurator AddDelegate<TSettings>(
this IConfigurator<TSettings>? configurator, this IConfigurator<TSettings>? configurator,
string name, string name,
Func<CommandContext, int> func) Func<CommandContext, CancellationToken, int> func)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
if (typeof(TSettings).IsAbstract) if (typeof(TSettings).IsAbstract)
@@ -358,7 +376,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
return configurator.AddDelegate<TSettings>(name, (c, _) => func(c)); return configurator.AddDelegate<TSettings>(name, (c, _, ct) => func(c, ct));
} }
/// <summary> /// <summary>
@@ -372,7 +390,7 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddAsyncDelegate<TSettings>( public static ICommandConfigurator AddAsyncDelegate<TSettings>(
this IConfigurator<TSettings> configurator, this IConfigurator<TSettings> configurator,
string name, string name,
Func<CommandContext, Task<int>> func) Func<CommandContext, CancellationToken, Task<int>> func)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
if (configurator == null) if (configurator == null)
@@ -380,7 +398,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
return configurator.AddAsyncDelegate<TSettings>(name, (c, _) => func(c)); return configurator.AddAsyncDelegate<TSettings>(name, (c, _, ct) => func(c, ct));
} }
/// <summary> /// <summary>

View File

@@ -18,6 +18,7 @@ public interface ICommand
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>The validation result.</returns> /// <returns>The validation result.</returns>
Task<int> Execute(CommandContext context, CommandSettings settings); Task<int> ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken);
} }

View File

@@ -15,13 +15,15 @@ public interface ICommandApp
/// Runs the command line application with specified arguments. /// Runs the command line application with specified arguments.
/// </summary> /// </summary>
/// <param name="args">The arguments.</param> /// <param name="args">The arguments.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the application.</param>
/// <returns>The exit code from the executed command.</returns> /// <returns>The exit code from the executed command.</returns>
int Run(IEnumerable<string> args); int Run(IEnumerable<string> args, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Runs the command line application with specified arguments. /// Runs the command line application with specified arguments.
/// </summary> /// </summary>
/// <param name="args">The arguments.</param> /// <param name="args">The arguments.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the application.</param>
/// <returns>The exit code from the executed command.</returns> /// <returns>The exit code from the executed command.</returns>
Task<int> RunAsync(IEnumerable<string> args); Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default);
} }

View File

@@ -88,6 +88,12 @@ public interface ICommandAppSettings
/// </summary> /// </summary>
bool PropagateExceptions { get; set; } bool PropagateExceptions { get; set; }
/// <summary>
/// Gets or sets the value used as the application exit code when it's aborted through the <see cref="CancellationToken"/>.
/// The default cancellation exit code is 130.
/// </summary>
int CancellationExitCode { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not examples should be validated. /// Gets or sets a value indicating whether or not examples should be validated.
/// </summary> /// </summary>

View File

@@ -12,6 +12,7 @@ public interface ICommand<TSettings> : ICommandLimiter<TSettings>
/// </summary> /// </summary>
/// <param name="context">The command context.</param> /// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns> /// <returns>An integer indicating whether or not the command executed successfully.</returns>
Task<int> Execute(CommandContext context, TSettings settings); Task<int> ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken);
} }

View File

@@ -48,7 +48,7 @@ public interface IConfigurator
/// <param name="name">The name of the command.</param> /// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param> /// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns> /// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, int> func) ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, int> func)
where TSettings : CommandSettings; where TSettings : CommandSettings;
/// <summary> /// <summary>
@@ -58,7 +58,7 @@ public interface IConfigurator
/// <param name="name">The name of the command.</param> /// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param> /// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns> /// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, Task<int>> func) ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, Task<int>> func)
where TSettings : CommandSettings; where TSettings : CommandSettings;
/// <summary> /// <summary>

View File

@@ -54,7 +54,7 @@ public interface IConfigurator<in TSettings>
/// <param name="name">The name of the command.</param> /// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param> /// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns> /// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func) ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, int> func)
where TDerivedSettings : TSettings; where TDerivedSettings : TSettings;
/// <summary> /// <summary>
@@ -64,7 +64,7 @@ public interface IConfigurator<in TSettings>
/// <param name="name">The name of the command.</param> /// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param> /// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns> /// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, Task<int>> func) ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, Task<int>> func)
where TDerivedSettings : TSettings; where TDerivedSettings : TSettings;
/// <summary> /// <summary>

View File

@@ -12,7 +12,7 @@ internal sealed class CommandExecutor
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
} }
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args) public async Task<int> ExecuteAsync(IConfiguration configuration, IEnumerable<string> args, CancellationToken cancellationToken)
{ {
CommandTreeParserResult parsedResult; CommandTreeParserResult parsedResult;
@@ -125,7 +125,7 @@ internal sealed class CommandExecutor
leaf.Command.Data); leaf.Command.Data);
// Execute the command tree. // Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); return await ExecuteAsync(leaf, parsedResult.Tree, context, resolver, configuration, cancellationToken).ConfigureAwait(false);
} }
} }
@@ -222,12 +222,13 @@ internal sealed class CommandExecutor
return (parsedResult, tokenizerResult); return (parsedResult, tokenizerResult);
} }
private static async Task<int> Execute( private static async Task<int> ExecuteAsync(
CommandTree leaf, CommandTree leaf,
CommandTree tree, CommandTree tree,
CommandContext context, CommandContext context,
ITypeResolver resolver, ITypeResolver resolver,
IConfiguration configuration) IConfiguration configuration,
CancellationToken cancellationToken)
{ {
try try
{ {
@@ -256,7 +257,7 @@ internal sealed class CommandExecutor
} }
// Execute the command. // Execute the command.
var result = await command.Execute(context, settings); var result = await command.ExecuteAsync(context, settings, cancellationToken);
foreach (var interceptor in interceptors) foreach (var interceptor in interceptors)
{ {
interceptor.InterceptResult(context, settings, ref result); interceptor.InterceptResult(context, settings, ref result);

View File

@@ -27,7 +27,7 @@ internal sealed class ExplainCommand : Command<ExplainCommand.Settings>, IBuiltI
public bool IncludeHidden { get; set; } public bool IncludeHidden { get; set; }
} }
public override int Execute(CommandContext context, Settings settings) public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
var tree = new Tree("CLI Configuration"); var tree = new Tree("CLI Configuration");
tree.AddNode(ValueMarkup("Application Name", _commandModel.ApplicationName, "no application name")); tree.AddNode(ValueMarkup("Application Name", _commandModel.ApplicationName, "no application name"));

View File

@@ -13,7 +13,7 @@ internal sealed class OpenCliGeneratorCommand : Command, IBuiltInCommand
_model = model ?? throw new ArgumentNullException(nameof(model)); _model = model ?? throw new ArgumentNullException(nameof(model));
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
var document = new OpenCliDocument var document = new OpenCliDocument
{ {

View File

@@ -11,7 +11,7 @@ internal sealed class VersionCommand : Command, IBuiltInCommand
_writer = configuration.Settings.Console.GetConsole(); _writer = configuration.Settings.Console.GetConsole();
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
_writer.MarkupLine( _writer.MarkupLine(
"[yellow]Spectre.Cli[/] version [aqua]{0}[/]", "[yellow]Spectre.Cli[/] version [aqua]{0}[/]",

View File

@@ -13,7 +13,7 @@ internal sealed class XmlDocCommand : Command, IBuiltInCommand
_writer = configuration.Settings.Console.GetConsole(); _writer = configuration.Settings.Console.GetConsole();
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
_writer.Write(Serialize(_model), Style.Plain); _writer.Write(Serialize(_model), Style.Plain);
return 0; return 0;

View File

@@ -18,6 +18,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public HelpProviderStyle? HelpProviderStyles { get; set; } public HelpProviderStyle? HelpProviderStyles { get; set; }
public bool StrictParsing { get; set; } public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; } public bool ConvertFlagsToRemainingArguments { get; set; }
public int CancellationExitCode { get; set; }
public ParsingMode ParsingMode => public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;
@@ -33,6 +34,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
TrimTrailingPeriod = true; TrimTrailingPeriod = true;
HelpProviderStyles = HelpProviderStyle.Default; HelpProviderStyles = HelpProviderStyle.Default;
ConvertFlagsToRemainingArguments = false; ConvertFlagsToRemainingArguments = false;
CancellationExitCode = 130;
} }
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName) public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@@ -56,19 +56,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }
public ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, int> func) public ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, int> func)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>( var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>(
name, (context, settings) => Task.FromResult(func(context, (TSettings)settings)))); name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TSettings)settings, cancellationToken))));
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }
public ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, Task<int>> func) public ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, Task<int>> func)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>( var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>(
name, (context, settings) => func(context, (TSettings)settings))); name, (context, settings, cancellationToken) => func(context, (TSettings)settings, cancellationToken)));
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }

View File

@@ -46,21 +46,21 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
return configurator; return configurator;
} }
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func) public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, int> func)
where TDerivedSettings : TSettings where TDerivedSettings : TSettings
{ {
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>( var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
name, (context, settings) => Task.FromResult(func(context, (TDerivedSettings)settings))); name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TDerivedSettings)settings, cancellationToken)));
_command.Children.Add(command); _command.Children.Add(command);
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }
public ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, Task<int>> func) public ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, Task<int>> func)
where TDerivedSettings : TSettings where TDerivedSettings : TSettings
{ {
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>( var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
name, (context, settings) => func(context, (TDerivedSettings)settings)); name, (context, settings, cancellationToken) => func(context, (TDerivedSettings)settings, cancellationToken));
_command.Children.Add(command); _command.Children.Add(command);
return new CommandConfigurator(command); return new CommandConfigurator(command);

View File

@@ -8,7 +8,7 @@ internal sealed class ConfiguredCommand
public object? Data { get; set; } public object? Data { get; set; }
public Type? CommandType { get; } public Type? CommandType { get; }
public Type SettingsType { get; } public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, Task<int>>? Delegate { get; } public Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? Delegate { get; }
public bool IsDefaultCommand { get; } public bool IsDefaultCommand { get; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
@@ -19,7 +19,7 @@ internal sealed class ConfiguredCommand
string name, string name,
Type? commandType, Type? commandType,
Type settingsType, Type settingsType,
Func<CommandContext, CommandSettings, Task<int>>? @delegate, Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? @delegate,
bool isDefaultCommand) bool isDefaultCommand)
{ {
Name = name; Name = name;
@@ -60,7 +60,7 @@ internal sealed class ConfiguredCommand
} }
public static ConfiguredCommand FromDelegate<TSettings>( public static ConfiguredCommand FromDelegate<TSettings>(
string name, Func<CommandContext, CommandSettings, Task<int>>? @delegate = null) string name, Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? @delegate = null)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
return new ConfiguredCommand(name, null, typeof(TSettings), @delegate, false); return new ConfiguredCommand(name, null, typeof(TSettings), @delegate, false);

View File

@@ -2,16 +2,16 @@ namespace Spectre.Console.Cli;
internal sealed class DelegateCommand : ICommand internal sealed class DelegateCommand : ICommand
{ {
private readonly Func<CommandContext, CommandSettings, Task<int>> _func; private readonly Func<CommandContext, CommandSettings, CancellationToken, Task<int>> _func;
public DelegateCommand(Func<CommandContext, CommandSettings, Task<int>> func) public DelegateCommand(Func<CommandContext, CommandSettings, CancellationToken, Task<int>> func)
{ {
_func = func; _func = func;
} }
public Task<int> Execute(CommandContext context, CommandSettings settings) public Task<int> ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{ {
return _func(context, settings); return _func(context, settings, cancellationToken);
} }
public ValidationResult Validate(CommandContext context, CommandSettings settings) public ValidationResult Validate(CommandContext context, CommandSettings settings)

View File

@@ -8,7 +8,7 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
public object? Data { get; } public object? Data { get; }
public Type? CommandType { get; } public Type? CommandType { get; }
public Type SettingsType { get; } public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, Task<int>>? Delegate { get; } public Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? Delegate { get; }
public bool IsDefaultCommand { get; } public bool IsDefaultCommand { get; }
public CommandInfo? Parent { get; } public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; } public IList<CommandInfo> Children { get; }

View File

@@ -98,7 +98,7 @@ public sealed class CommandAppTester
{ {
try try
{ {
Run(args, Console, c => c.PropagateExceptions()); RunAsync(args, Console, c => c.PropagateExceptions()).GetAwaiter().GetResult();
throw new InvalidOperationException("Expected an exception to be thrown, but there was none."); throw new InvalidOperationException("Expected an exception to be thrown, but there was none.");
} }
catch (T ex) catch (T ex)
@@ -129,53 +129,21 @@ public sealed class CommandAppTester
/// <returns>The result.</returns> /// <returns>The result.</returns>
public CommandAppResult Run(params string[] args) public CommandAppResult Run(params string[] args)
{ {
return Run(args, Console); return RunAsync(args, Console).GetAwaiter().GetResult();
}
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
{
CommandContext? context = null;
CommandSettings? settings = null;
var app = new CommandApp(Registrar);
_appConfiguration?.Invoke(app);
if (_configuration != null)
{
app.Configure(_configuration);
}
if (config != null)
{
app.Configure(config);
}
app.Configure(c => c.ConfigureConsole(console));
app.Configure(c => c.SetInterceptor(new CallbackCommandInterceptor((ctx, s) =>
{
context = ctx;
settings = s;
})));
var result = app.Run(args);
var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;
return new CommandAppResult(result, output, context, settings);
} }
/// <summary> /// <summary>
/// Runs the command application asynchronously. /// Runs the command application asynchronously.
/// </summary> /// </summary>
/// <param name="args">The arguments.</param> /// <param name="args">The arguments.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The result.</returns> /// <returns>The result.</returns>
public async Task<CommandAppResult> RunAsync(params string[] args) public async Task<CommandAppResult> RunAsync(string[]? args = null, CancellationToken cancellationToken = default)
{ {
return await RunAsync(args, Console); return await RunAsync(args ?? [], Console, cancellationToken: cancellationToken);
} }
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null) private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null, CancellationToken cancellationToken = default)
{ {
CommandContext? context = null; CommandContext? context = null;
CommandSettings? settings = null; CommandSettings? settings = null;
@@ -200,7 +168,7 @@ public sealed class CommandAppTester
settings = s; settings = s;
}))); })));
var result = await app.RunAsync(args); var result = await app.RunAsync(args, cancellationToken);
var output = console.Output.NormalizeLineEndings(); var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output; output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;

View File

@@ -9,10 +9,10 @@ public sealed class AsynchronousCommand : AsyncCommand<AsynchronousCommandSettin
_console = console; _console = console;
} }
public async override Task<int> ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings) public async override Task<int> ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings, CancellationToken cancellationToken)
{ {
// Simulate a long running asynchronous task // Simulate a long running asynchronous task
await Task.Delay(200); await Task.Delay(200, cancellationToken);
if (settings.ThrowException) if (settings.ThrowException)
{ {

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public class CatCommand : AnimalCommand<CatSettings> public class CatCommand : AnimalCommand<CatSettings>
{ {
public override int Execute(CommandContext context, CatSettings settings) public override int Execute(CommandContext context, CatSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -23,7 +23,7 @@ public class DogCommand : AnimalCommand<DogSettings>
return base.Validate(context, settings); return base.Validate(context, settings);
} }
public override int Execute(CommandContext context, DogSettings settings) public override int Execute(CommandContext context, DogSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -9,7 +9,7 @@ public sealed class DumpRemainingCommand : Command<EmptyCommandSettings>
_console = console; _console = console;
} }
public override int Execute(CommandContext context, EmptyCommandSettings settings) public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{ {
if (context.Remaining.Raw.Count > 0) if (context.Remaining.Raw.Count > 0)
{ {

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public sealed class EmptyCommand : Command<EmptyCommandSettings> public sealed class EmptyCommand : Command<EmptyCommandSettings>
{ {
public override int Execute(CommandContext context, EmptyCommandSettings settings) public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data;
public sealed class GenericCommand<TSettings> : Command<TSettings> public sealed class GenericCommand<TSettings> : Command<TSettings>
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
public override int Execute(CommandContext context, TSettings settings) public override int Execute(CommandContext context, TSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data;
[Description("The giraffe command.")] [Description("The giraffe command.")]
public sealed class GiraffeCommand : Command<GiraffeSettings> public sealed class GiraffeCommand : Command<GiraffeSettings>
{ {
public override int Execute(CommandContext context, GiraffeSettings settings) public override int Execute(CommandContext context, GiraffeSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -9,7 +9,7 @@ public class GreeterCommand : Command<OptionalArgumentWithDefaultValueSettings>
_console = console; _console = console;
} }
public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings) public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings, CancellationToken cancellationToken)
{ {
_console.WriteLine(settings.Greeting); _console.WriteLine(settings.Greeting);
return 0; return 0;

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public sealed class HiddenOptionsCommand : Command<HiddenOptionSettings> public sealed class HiddenOptionsCommand : Command<HiddenOptionSettings>
{ {
public override int Execute(CommandContext context, HiddenOptionSettings settings) public override int Execute(CommandContext context, HiddenOptionSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

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

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public sealed class InvalidCommand : Command<InvalidSettings> public sealed class InvalidCommand : Command<InvalidSettings>
{ {
public override int Execute(CommandContext context, InvalidSettings settings) public override int Execute(CommandContext context, InvalidSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data;
[Description("The lion command.")] [Description("The lion command.")]
public class LionCommand : AnimalCommand<LionSettings> public class LionCommand : AnimalCommand<LionSettings>
{ {
public override int Execute(CommandContext context, LionSettings settings) public override int Execute(CommandContext context, LionSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -5,7 +5,7 @@ public sealed class NoDescriptionCommand : Command<EmptyCommandSettings>
[CommandOption("-f|--foo <VALUE>")] [CommandOption("-f|--foo <VALUE>")]
public int Foo { get; set; } public int Foo { get; set; }
public override int Execute(CommandContext context, EmptyCommandSettings settings) public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public class OptionVectorCommand : Command<OptionVectorSettings> public class OptionVectorCommand : Command<OptionVectorSettings>
{ {
public override int Execute(CommandContext context, OptionVectorSettings settings) public override int Execute(CommandContext context, OptionVectorSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data;
public sealed class ThrowingCommand : Command<ThrowingCommandSettings> public sealed class ThrowingCommand : Command<ThrowingCommandSettings>
{ {
public override int Execute(CommandContext context, ThrowingCommandSettings settings) public override int Execute(CommandContext context, ThrowingCommandSettings settings, CancellationToken cancellationToken)
{ {
throw new InvalidOperationException("W00t?"); throw new InvalidOperationException("W00t?");
} }

View File

@@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data;
[Description("The turtle command.")] [Description("The turtle command.")]
public class TurtleCommand : AnimalCommand<TurtleSettings> public class TurtleCommand : AnimalCommand<TurtleSettings>
{ {
public override int Execute(CommandContext context, TurtleSettings settings) public override int Execute(CommandContext context, TurtleSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -9,7 +9,7 @@ public sealed class VersionCommand : Command<VersionSettings>
_console = console; _console = console;
} }
public override int Execute(CommandContext context, VersionSettings settings) public override int Execute(CommandContext context, VersionSettings settings, CancellationToken cancellationToken)
{ {
_console.WriteLine($"VersionCommand ran, Version: {settings.Version ?? string.Empty}"); _console.WriteLine($"VersionCommand ran, Version: {settings.Version ?? string.Empty}");

View File

@@ -5,6 +5,7 @@ global using System.Diagnostics.CodeAnalysis;
global using System.Globalization; global using System.Globalization;
global using System.Linq; global using System.Linq;
global using System.Runtime.CompilerServices; global using System.Runtime.CompilerServices;
global using System.Threading;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using Shouldly; global using Shouldly;
global using Spectre.Console.Cli; global using Spectre.Console.Cli;

View File

@@ -53,7 +53,7 @@ public sealed partial class CommandAppTests
}); });
// When // When
var result = await Record.ExceptionAsync(async () => var exception = await Record.ExceptionAsync(async () =>
await app.RunAsync(new[] await app.RunAsync(new[]
{ {
"--ThrowException", "--ThrowException",
@@ -61,10 +61,64 @@ public sealed partial class CommandAppTests
})); }));
// Then // Then
result.ShouldBeOfType<Exception>().And(ex => exception.ShouldBeOfType<Exception>().And(ex =>
{ {
ex.Message.ShouldBe("Throwing exception asynchronously"); ex.Message.ShouldBe("Throwing exception asynchronously");
}); });
} }
[Fact]
public async Task Should_Throw_OperationCanceledException_When_Propagated_And_Cancelled()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<AsynchronousCommand>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var exception = await Record.ExceptionAsync(async () =>
await app.RunAsync(cancellationToken: new CancellationToken(canceled: true)));
// Then
exception.ShouldNotBeNull();
exception.ShouldBeAssignableTo<OperationCanceledException>();
}
[Fact]
public async Task Should_Return_Default_Exit_Code_When_Cancelled()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<AsynchronousCommand>();
// When
var result = await app.RunAsync(cancellationToken: new CancellationToken(canceled: true));
// Then
result.ExitCode.ShouldBe(130);
result.Output.ShouldBeEmpty();
}
[Fact]
public async Task Should_Return_Custom_Exit_Code_When_Cancelled()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<AsynchronousCommand>();
app.Configure(config =>
{
config.CancellationExitCode(123);
});
// When
var result = await app.RunAsync(cancellationToken: new CancellationToken(canceled: true));
// Then
result.ExitCode.ShouldBe(123);
result.Output.ShouldBeEmpty();
}
} }
} }

View File

@@ -28,12 +28,12 @@ public sealed partial class CommandAppTests
public class NullableCommand : Command<NullableSettings> public class NullableCommand : Command<NullableSettings>
{ {
public override int Execute(CommandContext context, NullableSettings settings) => 0; public override int Execute(CommandContext context, NullableSettings settings, CancellationToken cancellationToken) => 0;
} }
public class NullableWithInitCommand : Command<NullableWithInitSettings> public class NullableWithInitCommand : Command<NullableWithInitSettings>
{ {
public override int Execute(CommandContext context, NullableWithInitSettings settings) => 0; public override int Execute(CommandContext context, NullableWithInitSettings settings, CancellationToken cancellationToken) => 0;
} }
[Fact] [Fact]

View File

@@ -19,7 +19,7 @@ public sealed partial class CommandAppTests
_dep = dep; _dep = dep;
} }
public override int Execute(CommandContext context, CustomInheritedCommandSettings settings) public override int Execute(CommandContext context, CustomInheritedCommandSettings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -10,7 +10,7 @@ public sealed partial class CommandAppTests
{ {
} }
public override int Execute(CommandContext context, Settings settings) public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{ {
return 0; return 0;
} }

View File

@@ -1117,7 +1117,7 @@ public sealed partial class CommandAppTests
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddDelegate<DogSettings>( config.AddDelegate<DogSettings>(
"foo", (context, settings) => "foo", (context, settings, _) =>
{ {
dog = settings; dog = settings;
data = (int)context.Data; data = (int)context.Data;
@@ -1145,7 +1145,7 @@ public sealed partial class CommandAppTests
{ {
cfg.AddBranch("a", d => cfg.AddBranch("a", d =>
{ {
d.AddDelegate("b", _ => 0); d.AddDelegate("b", (_, _) => 0);
}); });
}); });
@@ -1165,7 +1165,7 @@ public sealed partial class CommandAppTests
var app = new CommandAppTester(); var app = new CommandAppTester();
app.Configure(cfg => app.Configure(cfg =>
{ {
cfg.AddDelegate("a", _ => 0); cfg.AddDelegate("a", (_, _) => 0);
}); });
// When // When
@@ -1189,7 +1189,7 @@ public sealed partial class CommandAppTests
{ {
config.PropagateExceptions(); config.PropagateExceptions();
config.AddAsyncDelegate<DogSettings>( config.AddAsyncDelegate<DogSettings>(
"foo", (context, settings) => "foo", (context, settings, _) =>
{ {
dog = settings; dog = settings;
data = (int)context.Data; data = (int)context.Data;
@@ -1222,7 +1222,7 @@ public sealed partial class CommandAppTests
config.AddBranch<AnimalSettings>("foo", foo => config.AddBranch<AnimalSettings>("foo", foo =>
{ {
foo.AddDelegate<DogSettings>( foo.AddDelegate<DogSettings>(
"bar", (context, settings) => "bar", (context, settings, _) =>
{ {
dog = settings; dog = settings;
data = (int)context.Data; data = (int)context.Data;
@@ -1256,7 +1256,7 @@ public sealed partial class CommandAppTests
config.AddBranch<AnimalSettings>("foo", foo => config.AddBranch<AnimalSettings>("foo", foo =>
{ {
foo.AddAsyncDelegate<DogSettings>( foo.AddAsyncDelegate<DogSettings>(
"bar", (context, settings) => "bar", (context, settings, _) =>
{ {
dog = settings; dog = settings;
data = (int)context.Data; data = (int)context.Data;

View File

@@ -14,7 +14,7 @@ public sealed class CommandAppTesterTests
_console = console; _console = console;
} }
public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings) public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings, CancellationToken cancellationToken)
{ {
_console.Write(settings.Greeting); _console.Write(settings.Greeting);
return 0; return 0;

View File

@@ -11,7 +11,7 @@ public sealed class InteractiveCommandTests
_console = console; _console = console;
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context, CancellationToken cancellationToken)
{ {
var fruits = _console.Prompt( var fruits = _console.Prompt(
new MultiSelectionPrompt<string>() new MultiSelectionPrompt<string>()