diff --git a/CliFx.Benchmarks/Commands/CliFxCommand.cs b/CliFx.Benchmarks/Commands/CliFxCommand.cs index bb2e0b2..3c33aff 100644 --- a/CliFx.Benchmarks/Commands/CliFxCommand.cs +++ b/CliFx.Benchmarks/Commands/CliFxCommand.cs @@ -17,6 +17,6 @@ namespace CliFx.Benchmarks.Commands [CommandOption("bool", 'b')] public bool BoolOption { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookAddCommand.cs b/CliFx.Demo/Commands/BookAddCommand.cs index 5f7a2fc..9bc8bcd 100644 --- a/CliFx.Demo/Commands/BookAddCommand.cs +++ b/CliFx.Demo/Commands/BookAddCommand.cs @@ -32,7 +32,7 @@ namespace CliFx.Demo.Commands _libraryService = libraryService; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { // To make the demo simpler, we will just generate random publish date and ISBN if they were not set if (Published == default) diff --git a/CliFx.Demo/Commands/BookCommand.cs b/CliFx.Demo/Commands/BookCommand.cs index 4cc6c28..406f695 100644 --- a/CliFx.Demo/Commands/BookCommand.cs +++ b/CliFx.Demo/Commands/BookCommand.cs @@ -21,7 +21,7 @@ namespace CliFx.Demo.Commands _libraryService = libraryService; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { var book = _libraryService.GetBook(Title); diff --git a/CliFx.Demo/Commands/BookListCommand.cs b/CliFx.Demo/Commands/BookListCommand.cs index cd333b8..2035bfd 100644 --- a/CliFx.Demo/Commands/BookListCommand.cs +++ b/CliFx.Demo/Commands/BookListCommand.cs @@ -17,7 +17,7 @@ namespace CliFx.Demo.Commands _libraryService = libraryService; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { var library = _libraryService.GetLibrary(); diff --git a/CliFx.Demo/Commands/BookRemoveCommand.cs b/CliFx.Demo/Commands/BookRemoveCommand.cs index ce33c4f..83047f2 100644 --- a/CliFx.Demo/Commands/BookRemoveCommand.cs +++ b/CliFx.Demo/Commands/BookRemoveCommand.cs @@ -20,7 +20,7 @@ namespace CliFx.Demo.Commands _libraryService = libraryService; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { var book = _libraryService.GetBook(Title); diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index b14985c..9547872 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using CliFx.Services; using CliFx.Tests.Stubs; @@ -237,8 +238,9 @@ namespace CliFx.Tests { // Arrange using (var stdoutStream = new StringWriter()) + using (var cancellationTokenSource = new CancellationTokenSource()) { - var console = new VirtualConsole(stdoutStream); + var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token); var application = new CliApplicationBuilder() .AddCommand(typeof(CancellableCommand)) @@ -248,7 +250,7 @@ namespace CliFx.Tests // Act var runTask = application.RunAsync(args); - console.Cancel(); + cancellationTokenSource.Cancel(); var exitCode = await runTask.ConfigureAwait(false); var stdOut = stdoutStream.ToString().Trim(); diff --git a/CliFx.Tests/TestCommands/CancellableCommand.cs b/CliFx.Tests/TestCommands/CancellableCommand.cs index 4623afe..5b6cbfd 100644 --- a/CliFx.Tests/TestCommands/CancellableCommand.cs +++ b/CliFx.Tests/TestCommands/CancellableCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Services; @@ -9,13 +8,13 @@ namespace CliFx.Tests.TestCommands [Command("cancel")] public class CancellableCommand : ICommand { - public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public async Task ExecuteAsync(IConsole console) { await Task.Yield(); console.Output.WriteLine("Printed"); - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1), console.GetCancellationToken()).ConfigureAwait(false); console.Output.WriteLine("Never printed"); } diff --git a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs b/CliFx.Tests/TestCommands/CommandExceptionCommand.cs index 1502680..6e047ab 100644 --- a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs +++ b/CliFx.Tests/TestCommands/CommandExceptionCommand.cs @@ -15,6 +15,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("msg", 'm')] public string Message { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => throw new CommandException(Message, ExitCode); + public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode); } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/ConcatCommand.cs b/CliFx.Tests/TestCommands/ConcatCommand.cs index e1fde71..48a65f9 100644 --- a/CliFx.Tests/TestCommands/ConcatCommand.cs +++ b/CliFx.Tests/TestCommands/ConcatCommand.cs @@ -15,7 +15,7 @@ namespace CliFx.Tests.TestCommands [CommandOption('s', Description = "String separator.")] public string Separator { get; set; } = ""; - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { console.Output.WriteLine(string.Join(Separator, Inputs)); return Task.CompletedTask; diff --git a/CliFx.Tests/TestCommands/DivideCommand.cs b/CliFx.Tests/TestCommands/DivideCommand.cs index 69caefd..d29fe24 100644 --- a/CliFx.Tests/TestCommands/DivideCommand.cs +++ b/CliFx.Tests/TestCommands/DivideCommand.cs @@ -17,7 +17,7 @@ namespace CliFx.Tests.TestCommands // This property should be ignored by resolver public bool NotAnOption { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { console.Output.WriteLine(Dividend / Divisor); return Task.CompletedTask; diff --git a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs index 8703994..3745658 100644 --- a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs +++ b/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs @@ -14,6 +14,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("fruits")] public string Oranges { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs index ee4c758..41d2599 100644 --- a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs +++ b/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs @@ -14,6 +14,6 @@ namespace CliFx.Tests.TestCommands [CommandOption('f')] public string Oranges { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs index 654dd57..33ab651 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs @@ -11,6 +11,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] public string Option { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs index ca9a379..653cc19 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs @@ -12,6 +12,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] public IEnumerable Option { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs index f1d6f5b..56f4b13 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs @@ -12,6 +12,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] public string Option { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } diff --git a/CliFx.Tests/TestCommands/ExceptionCommand.cs b/CliFx.Tests/TestCommands/ExceptionCommand.cs index 6e9f8d5..4eb0417 100644 --- a/CliFx.Tests/TestCommands/ExceptionCommand.cs +++ b/CliFx.Tests/TestCommands/ExceptionCommand.cs @@ -12,6 +12,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("msg", 'm')] public string Message { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => throw new Exception(Message); + public Task ExecuteAsync(IConsole console) => throw new Exception(Message); } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs b/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs index 201e9e0..b83685e 100644 --- a/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs +++ b/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs @@ -8,7 +8,7 @@ namespace CliFx.Tests.TestCommands [Command] public class HelloWorldDefaultCommand : ICommand { - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { console.Output.WriteLine("Hello world."); return Task.CompletedTask; diff --git a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs b/CliFx.Tests/TestCommands/HelpDefaultCommand.cs index 627489e..54ac7fb 100644 --- a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs +++ b/CliFx.Tests/TestCommands/HelpDefaultCommand.cs @@ -14,6 +14,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("option-b", 'b', Description = "OptionB description.")] public string OptionB { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpNamedCommand.cs b/CliFx.Tests/TestCommands/HelpNamedCommand.cs index cf4c110..840be37 100644 --- a/CliFx.Tests/TestCommands/HelpNamedCommand.cs +++ b/CliFx.Tests/TestCommands/HelpNamedCommand.cs @@ -14,6 +14,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("option-d", 'd', Description = "OptionD description.")] public string OptionD { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpSubCommand.cs b/CliFx.Tests/TestCommands/HelpSubCommand.cs index 1cb8e14..602ab48 100644 --- a/CliFx.Tests/TestCommands/HelpSubCommand.cs +++ b/CliFx.Tests/TestCommands/HelpSubCommand.cs @@ -11,6 +11,6 @@ namespace CliFx.Tests.TestCommands [CommandOption("option-e", 'e', Description = "OptionE description.")] public string OptionE { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs b/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs index 76747b9..b00b7b8 100644 --- a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs +++ b/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs @@ -6,6 +6,6 @@ namespace CliFx.Tests.TestCommands { public class NonAnnotatedCommand : ICommand { - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console) => Task.CompletedTask; } } \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index ef81056..91cf4b1 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -171,7 +171,7 @@ namespace CliFx _commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput); // Execute command - await command.ExecuteAsync(_console, _console.CancellationToken); + await command.ExecuteAsync(_console); // Finish the chain with exit code 0 return 0; diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index f8efef7..9d7f53a 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -13,6 +13,6 @@ namespace CliFx /// Executes command using specified implementation of . /// This method is called when the command is invoked by a user through command line interface. /// - Task ExecuteAsync(IConsole console, CancellationToken cancellationToken); + Task ExecuteAsync(IConsole console); } } \ No newline at end of file diff --git a/CliFx/Services/IConsole.cs b/CliFx/Services/IConsole.cs index 94d670f..de85fe6 100644 --- a/CliFx/Services/IConsole.cs +++ b/CliFx/Services/IConsole.cs @@ -55,8 +55,9 @@ namespace CliFx.Services void ResetColor(); /// - /// Cancels when soft cancellation requested. + /// Provides token that cancels when application cancellation is requested. + /// Subsequent calls return the same token. /// - CancellationToken CancellationToken { get; } + CancellationToken GetCancellationToken(); } } \ No newline at end of file diff --git a/CliFx/Services/SystemConsole.cs b/CliFx/Services/SystemConsole.cs index 7604d87..47456e5 100644 --- a/CliFx/Services/SystemConsole.cs +++ b/CliFx/Services/SystemConsole.cs @@ -9,21 +9,7 @@ namespace CliFx.Services /// public class SystemConsole : IConsole { - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - /// - public SystemConsole() - { - // Subscribe to CancelKeyPress event with cancellation token source - // Kills app on second cancellation (hard cancellation) - Console.CancelKeyPress += (_, args) => - { - if (_cancellationTokenSource.IsCancellationRequested) - return; - args.Cancel = true; - _cancellationTokenSource.Cancel(); - }; - } + private CancellationTokenSource _cancellationTokenSource; /// public TextReader Input => Console.In; @@ -61,6 +47,24 @@ namespace CliFx.Services public void ResetColor() => Console.ResetColor(); /// - public CancellationToken CancellationToken => _cancellationTokenSource.Token; + public CancellationToken GetCancellationToken() + { + if (_cancellationTokenSource is null) + { + _cancellationTokenSource = new CancellationTokenSource(); + + // Subscribe to CancelKeyPress event with cancellation token source + // Kills app on second cancellation (hard cancellation) + Console.CancelKeyPress += (_, args) => + { + if (_cancellationTokenSource.IsCancellationRequested) + return; + args.Cancel = true; + _cancellationTokenSource.Cancel(); + }; + } + + return _cancellationTokenSource.Token; + } } } \ No newline at end of file diff --git a/CliFx/Services/VirtualConsole.cs b/CliFx/Services/VirtualConsole.cs index b27a150..73ce481 100644 --- a/CliFx/Services/VirtualConsole.cs +++ b/CliFx/Services/VirtualConsole.cs @@ -12,7 +12,7 @@ namespace CliFx.Services /// public class VirtualConsole : IConsole { - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken _cancellationToken; /// public TextReader Input { get; } @@ -43,7 +43,8 @@ namespace CliFx.Services /// public VirtualConsole(TextReader input, bool isInputRedirected, TextWriter output, bool isOutputRedirected, - TextWriter error, bool isErrorRedirected) + TextWriter error, bool isErrorRedirected, + CancellationToken cancellationToken = default) { Input = input.GuardNotNull(nameof(input)); IsInputRedirected = isInputRedirected; @@ -51,13 +52,15 @@ namespace CliFx.Services IsOutputRedirected = isOutputRedirected; Error = error.GuardNotNull(nameof(error)); IsErrorRedirected = isErrorRedirected; + _cancellationToken = cancellationToken; } /// /// Initializes an instance of . /// - public VirtualConsole(TextReader input, TextWriter output, TextWriter error) - : this(input, true, output, true, error, true) + public VirtualConsole(TextReader input, TextWriter output, TextWriter error, + CancellationToken cancellationToken = default) + : this(input, true, output, true, error, true, cancellationToken) { } @@ -65,8 +68,8 @@ namespace CliFx.Services /// Initializes an instance of using output stream (stdout) and error stream (stderr). /// Input stream (stdin) is replaced with a no-op stub. /// - public VirtualConsole(TextWriter output, TextWriter error) - : this(TextReader.Null, output, error) + public VirtualConsole(TextWriter output, TextWriter error, CancellationToken cancellationToken = default) + : this(TextReader.Null, output, error, cancellationToken) { } @@ -74,8 +77,8 @@ namespace CliFx.Services /// Initializes an instance of using output stream (stdout). /// Input stream (stdin) and error stream (stderr) are replaced with no-op stubs. /// - public VirtualConsole(TextWriter output) - : this(output, TextWriter.Null) + public VirtualConsole(TextWriter output, CancellationToken cancellationToken = default) + : this(output, TextWriter.Null, cancellationToken) { } @@ -87,14 +90,6 @@ namespace CliFx.Services } /// - public CancellationToken CancellationToken => _cancellationTokenSource.Token; - - /// - /// Simulates cancellation. - /// - public void Cancel() - { - _cancellationTokenSource.Cancel(); - } + public CancellationToken GetCancellationToken() => _cancellationToken; } } \ No newline at end of file diff --git a/Readme.md b/Readme.md index 7f819ae..b36231f 100644 --- a/Readme.md +++ b/Readme.md @@ -24,6 +24,7 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._ - Resolves commands and options using attributes - Handles options of various types, including custom types - Supports multi-level command hierarchies +- Allows cancellation - Generates contextual help text - Prints errors and routes exit codes on exceptions - Highly testable and easy to debug @@ -87,7 +88,7 @@ public class LogCommand : ICommand [CommandOption("base", 'b', Description = "Logarithm base.")] public double Base { get; set; } = 10; - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { var result = Math.Log(Value, Base); console.Output.WriteLine(result); @@ -99,10 +100,7 @@ public class LogCommand : ICommand By implementing `ICommand` this class also provides `ExecuteAsync` method. This is the method that gets called when the user invokes the command. Its return type is `Task` in order to facilitate asynchronous execution, but if your command runs synchronously you can simply return `Task.CompletedTask`. -This method takes two parameters: an instance of `IConsole` and `CancellationToken`. - -You should use the `console` parameter in places where you would normally use `System.Console`, in order to make your command testable. -The `cancellationToken` parameter can be used to handle interrupt signal (Ctrl+C or Ctrl+Break) to gracefully cancel execution and perform any necessary cleanup. If another interrupt signal is received after the first one, the application will terminate immediately. +The `ExecuteAsync` method also takes an instance of `IConsole` as a parameter. You should use the `console` parameter in places where you would normally use `System.Console`, in order to make your command testable. Finally, the command defined above can be executed from the command line in one of the following ways: @@ -176,7 +174,7 @@ public class DivideCommand : ICommand [CommandOption("divisor", IsRequired = true)] public double Divisor { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { if (Math.Abs(Divisor) < double.Epsilon) { @@ -218,6 +216,30 @@ public class SecondSubCommand : ICommand } ``` +### Cancellation + +It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break). You can call `console.GetCancellationToken()` to override the default behavior and get `CancellationToken` that represents the first interrupt signal. Second interrupt signal terminates an app immediately. Note that the code that executes before the first call to `GetCancellationToken` will not be cancellation aware. + +You can pass `CancellationToken` around and check its state. + +Cancelled or terminated app returns non-zero exit code. + +```c# +[Command("cancel")] +public class CancellableCommand : ICommand +{ + public async Task ExecuteAsync(IConsole console) + { + console.Output.WriteLine("Printed"); + + // Long-running cancellable operation that throws when canceled + await Task.Delay(Timeout.InfiniteTimeSpan, console.GetCancellationToken()); + + console.Output.WriteLine("Never printed"); + } +} +``` + ### Dependency injection CliFx uses an implementation of `ICommandFactory` to initialize commands and by default it only works with types that have parameterless constructors. @@ -266,7 +288,7 @@ public class UserAddCommand : ICommand [CommandOption("email", 'e')] public string Email { get; set; } - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { var validationResult = new UserAddCommandValidator().Validate(this); if (!validationResult.IsValid) @@ -339,7 +361,7 @@ public class ConcatCommand : ICommand [CommandOption("right")] public string Right { get; set; } = "world"; - public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) + public Task ExecuteAsync(IConsole console) { console.Output.Write(Left); console.Output.Write(' '); @@ -368,7 +390,7 @@ public async Task ConcatCommand_Test() }; // Act - await command.ExecuteAsync(console, CancellationToken.None); + await command.ExecuteAsync(console); // Assert Assert.That(stdout.ToString(), Is.EqualTo("foo bar"));