Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI actions prototype #2065

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>net7.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net462</TargetFrameworks>
<LangVersion>10</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<IsPackable>true</IsPackable>
<PackageId>System.CommandLine.NamingConventionBinder</PackageId>
<TargetFrameworks>net7.0;netstandard2.0</TargetFrameworks>
<LangVersion>10</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Description>This package provides command handler support for System.CommandLine performs parameter and model binding by matching option and argument names to parameter and property names.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
1 change: 0 additions & 1 deletion src/System.CommandLine.Tests/ArgumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using System.CommandLine.Completions;

namespace System.CommandLine.Tests
{
Expand Down
129 changes: 129 additions & 0 deletions src/System.CommandLine.Tests/Invocation/CliActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace System.CommandLine.Tests.Invocation;

public class CliActionTests
{
private readonly TestConsole _console = new();

private static RootCommand CreateRootCommand()
{
var subcommandOne = new Command("one");

var root = new RootCommand
{
subcommandOne
};

subcommandOne.SetAction(ctx => ctx.Console.Write("hello from one"));
root.SetAction(ctx => ctx.Console.Write("hello from root"));

return root;
}

[Fact]
public async Task root_command_handler_base_case()
{
var root = CreateRootCommand();

var parseResult = root.Parse("");

await parseResult.Action.RunAsync(_console);

_console.Out.ToString().Should().Be("hello from root");
}

[Fact]
public async Task subcommand_handler_base_case()
{
var root = CreateRootCommand();

var parseResult = root.Parse("one");

await parseResult.Action.RunAsync(_console);

_console.Out.ToString().Should().Be("hello from one");
}

[Fact]
public async Task root_command_help_option_base_case()
{
var root = CreateRootCommand();

var parseResult = root.Parse("-h");

await parseResult.Action.RunAsync(_console);

_console.Out.ToString().Should().Match($"*Usage:*{RootCommand.ExecutableName}*");
}

[Fact]
public async Task subcommand_help_option_base_case()
{
var root = CreateRootCommand();

var parseResult = root.Parse("one -h");

await parseResult.Action.RunAsync(_console);

_console.Out.ToString().Should().Match("*Usage:*one*");
}

[Fact]
public async Task instead_of_middleware_a_switch_statement_can_be_used_to_intercept_default_behaviors()
{
var root = CreateRootCommand();

var parseResult = root.Parse("one -h");

switch (parseResult.Action)
{
case HelpAction helpAction:
_console.WriteLine("START");
await helpAction.RunAsync(_console);
_console.WriteLine("END");
break;

default:
await parseResult.Action.RunAsync(_console);
break;
}

_console.Out.ToString().Should().Match("START*Usage:*one*END*");
}

[Fact]
public void user_defined_types_can_be_used()
{
var commandOne = new Command("one");

var commandTwo = new Command("two");

var root = new RootCommand
{
commandOne,
commandTwo
};

commandOne.SetAction(new CustomActionOne());
commandTwo.SetAction(new CustomActionTwo());

root.Parse("one").Action.Should().BeOfType<CustomActionOne>();
}
}

public class CustomActionOne : CommandAction
{
}

public class CustomActionTwo : CommandAction
{
}
1 change: 1 addition & 0 deletions src/System.CommandLine/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections;
using System.Collections.Generic;
using System.CommandLine.Completions;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.ComponentModel;
using System.Linq;
Expand Down
11 changes: 10 additions & 1 deletion src/System.CommandLine/Help/HelpOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal HelpOption() : this(new[]

public override int GetHashCode() => typeof(HelpOption).GetHashCode();

internal static void Handler(InvocationContext context)
internal void Handler(InvocationContext context)
{
var output = context.Console.Out.CreateTextWriter();

Expand All @@ -42,5 +42,14 @@ internal static void Handler(InvocationContext context)

context.HelpBuilder.Write(helpContext);
}

internal HelpAction Action => new(this);
}

public class HelpAction : CliAction
{
internal HelpAction(HelpOption helpOption) : base(new AnonymousCommandHandler(helpOption.Handler))
{
}
}
}
81 changes: 81 additions & 0 deletions src/System.CommandLine/Invocation/InvocationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.IO;
using System.Threading;
using System.Threading.Tasks;

#pragma warning disable CS1591

namespace System.CommandLine.Invocation;

public abstract class CliAction : ICommandHandler
{
private readonly ICommandHandler _innerHandler;
private ParseResult? _parseResult;

protected CliAction()
{
}

internal CliAction(ICommandHandler innerHandler)
{
_innerHandler = innerHandler;
}

int ICommandHandler.Invoke(InvocationContext context) =>
_innerHandler.Invoke(context);

Task<int> ICommandHandler.InvokeAsync(InvocationContext context, CancellationToken cancellationToken) =>
_innerHandler.InvokeAsync(context, cancellationToken);

public async Task<int> RunAsync(
IConsole? console = null,
CancellationToken? cancellationToken = null)
{
var invocationContext = new InvocationContext(
ParseResult,
console ?? new SystemConsole());

return await _innerHandler.InvokeAsync(
invocationContext,
cancellationToken ?? CancellationToken.None);
}

internal ParseResult ParseResult
{
get => _parseResult ?? ParseResult.Empty();
set => _parseResult = value;
}
}

internal class AnonymousCommandAction : CommandAction
{
public AnonymousCommandAction(Action<InvocationContext> invoke) : base(new AnonymousCommandHandler(invoke))
{
}
}

public abstract class CommandAction : CliAction
{
protected CommandAction()
{
}

protected CommandAction(ICommandHandler innerHandler) : base(innerHandler)
{
}
}

public static class CommandExtensions
{
public static void SetAction(this Command command, CommandAction handler)
{
command.Handler = handler;
}

public static void SetAction(this Command command, Action<InvocationContext> invoke)
{
command.SetAction(new AnonymousCommandAction(invoke));
}
}
2 changes: 1 addition & 1 deletion src/System.CommandLine/Invocation/ParseErrorResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static void Apply(InvocationContext context)

context.Console.ResetTerminalForegroundColor();

HelpOption.Handler(context);
new HelpOption().Handler(context);
}
}
}
28 changes: 26 additions & 2 deletions src/System.CommandLine/ParseResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class ParseResult
private Dictionary<string, IReadOnlyList<string>>? _directives;
private CompletionContext? _completionContext;
private ICommandHandler? _handler;
private CliAction? _action;

internal ParseResult(
CommandLineConfiguration configuration,
Expand Down Expand Up @@ -255,12 +256,35 @@ internal ICommandHandler? Handler

if (CommandResult.Command is { } command)
{
return command.Handler;
return _handler ??= command.Handler;
}

return null;
}
set => _handler = value;
}

public CliAction Action
{
get
{
if (_action is null)
{
var handler = Handler;

if (handler is null)
{
return null;
}

if (handler is CliAction action)
{
_action = action;
_action.ParseResult = this;
}
}

return _action;
}
}

private SymbolResult SymbolToComplete(int? position = null)
Expand Down
4 changes: 2 additions & 2 deletions src/System.CommandLine/Parsing/ParseOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ private void ParseOption()
// parse directive has a precedence over --help and --version
if (!_isParseRequested)
{
if (option is HelpOption)
if (option is HelpOption helpOption)
{
_isHelpRequested = true;

_handler = new AnonymousCommandHandler(HelpOption.Handler);
_handler = helpOption.Action;
}
else if (option is VersionOption)
{
Expand Down
2 changes: 1 addition & 1 deletion src/System.CommandLine/System.CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<TargetFrameworks>net7.0;netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>10</LangVersion>
<LangVersion>latest</LangVersion>
<Description>This package includes a powerful command line parser and other tools for building command line applications, including:

* Shell-agnostic support for command line completions
Expand Down