Skip to content

Commit

Permalink
WIP: add support for nested sub-commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mayuki committed Feb 6, 2020
1 parent 0cc2504 commit 2569752
Show file tree
Hide file tree
Showing 19 changed files with 231 additions and 41 deletions.
25 changes: 25 additions & 0 deletions samples/GettingStarted.SubCommandApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,30 @@ public void Bye([Option('l', Description = "Print a name converted to lower-case
{
Console.WriteLine($"Goodbye {(toLowerCase ? name.ToLower() : name)}!");
}

[SubCommands(typeof(SubCommands))]
public void SubCommand()
{ }
}

class SubCommands
{
public void Konnichiwa()
{
Console.WriteLine("Konnichiwa!");
}

[SubCommands(typeof(SubSubCommands))]
public void SubSubCommand()
{
}
}

class SubSubCommands
{
public void Hauhau()
{
Console.WriteLine("Hauhau!");
}
}
}
8 changes: 7 additions & 1 deletion src/Cocona.Core/CoconaAppContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ public class CoconaAppContext
/// </summary>
public CoconaAppFeatureCollection Features { get; }

public CoconaAppContext(CancellationToken cancellationToken)
/// <summary>
/// Gets a executing command.
/// </summary>
public CommandDescriptor ExecutingCommand { get; }

public CoconaAppContext(CommandDescriptor command, CancellationToken cancellationToken)
{
ExecutingCommand = command;
CancellationToken = cancellationToken;
Features = new CoconaAppFeatureCollection();
}
Expand Down
11 changes: 9 additions & 2 deletions src/Cocona.Core/Command/BuiltIn/BuiltInCommandMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Cocona.Command.Features;

namespace Cocona.Command.BuiltIn
{
Expand All @@ -16,23 +17,29 @@ public class BuiltInCommandMiddleware : CommandDispatcherMiddleware
private readonly ICoconaCommandHelpProvider _commandHelpProvider;
private readonly ICoconaCommandProvider _commandProvider;
private readonly ICoconaConsoleProvider _console;
private readonly ICoconaAppContextAccessor _appContext;

public BuiltInCommandMiddleware(CommandDispatchDelegate next, ICoconaHelpRenderer helpRenderer, ICoconaCommandHelpProvider commandHelpProvider, ICoconaCommandProvider commandProvider, ICoconaConsoleProvider console)
public BuiltInCommandMiddleware(CommandDispatchDelegate next, ICoconaHelpRenderer helpRenderer, ICoconaCommandHelpProvider commandHelpProvider, ICoconaCommandProvider commandProvider, ICoconaConsoleProvider console, ICoconaAppContextAccessor appContext)
: base(next)
{
_helpRenderer = helpRenderer;
_commandHelpProvider = commandHelpProvider;
_commandProvider = commandProvider;
_console = console;
_appContext = appContext;
}

public override ValueTask<int> DispatchAsync(CommandDispatchContext ctx)
{
var hasHelpOption = ctx.ParsedCommandLine.Options.Any(x => x.Option == BuiltInCommandOption.Help);
if (hasHelpOption)
{
var feature = _appContext.Current?.Features.Get<ICoconaCommandFeature>();
var commandCollection = feature?.CommandCollection
?? _commandProvider.GetCommandCollection();

var help = (ctx.Command.IsPrimaryCommand)
? _commandHelpProvider.CreateCommandsIndexHelp(_commandProvider.GetCommandCollection())
? _commandHelpProvider.CreateCommandsIndexHelp(commandCollection, feature)
: _commandHelpProvider.CreateCommandHelp(ctx.Command);

_console.Output.Write(_helpRenderer.Render(help));
Expand Down
13 changes: 10 additions & 3 deletions src/Cocona.Core/Command/BuiltIn/BuiltInPrimaryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@
using Cocona.Help;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Cocona.Command.Features;

namespace Cocona.Command.BuiltIn
{
public class BuiltInPrimaryCommand
{
private readonly ICoconaAppContextAccessor _appContext;
private readonly ICoconaConsoleProvider _console;
private readonly ICoconaCommandHelpProvider _commandHelpProvider;
private readonly ICoconaHelpRenderer _helpRenderer;
private readonly ICoconaCommandProvider _commandProvider;
private static readonly MethodInfo _methodShowDefaultMessage = typeof(BuiltInPrimaryCommand).GetMethod(nameof(ShowDefaultMessage), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

public BuiltInPrimaryCommand(ICoconaConsoleProvider console, ICoconaCommandHelpProvider commandHelpProvider, ICoconaHelpRenderer helpRenderer, ICoconaCommandProvider commandProvider)
public BuiltInPrimaryCommand(ICoconaAppContextAccessor appContext, ICoconaConsoleProvider console, ICoconaCommandHelpProvider commandHelpProvider, ICoconaHelpRenderer helpRenderer, ICoconaCommandProvider commandProvider)
{
_appContext = appContext;
_console = console;
_commandHelpProvider = commandHelpProvider;
_helpRenderer = helpRenderer;
Expand All @@ -34,13 +38,16 @@ public static CommandDescriptor GetCommand(string description)
Array.Empty<CommandOptionDescriptor>(),
Array.Empty<CommandArgumentDescriptor>(),
Array.Empty<CommandOverloadDescriptor>(),
CommandFlags.Primary
CommandFlags.Primary,
null
);
}

private void ShowDefaultMessage()
{
_console.Output.Write(_helpRenderer.Render(_commandHelpProvider.CreateCommandsIndexHelp(_commandProvider.GetCommandCollection())));
var commandStack = _appContext.Current?.Features.Get<ICoconaNestedCommandFeature>()?.CommandStack;
var commandCollection = commandStack?.LastOrDefault()?.SubCommands ?? _commandProvider.GetCommandCollection();
_console.Output.Write(_helpRenderer.Render(_commandHelpProvider.CreateCommandsIndexHelp(commandCollection, commandStack)));
}

public static bool IsBuiltInCommand(CommandDescriptor command)
Expand Down
13 changes: 7 additions & 6 deletions src/Cocona.Core/Command/BuiltIn/CoconaBuiltInCommandProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ namespace Cocona.Command.BuiltIn
public class CoconaBuiltInCommandProvider : ICoconaCommandProvider
{
private readonly ICoconaCommandProvider _underlyingCommandProvider;
private readonly Lazy<CommandCollection> _commandCollection;
private CommandCollection? _cachedCommandCollection;

public CoconaBuiltInCommandProvider(ICoconaCommandProvider underlyingCommandProvider)
{
_underlyingCommandProvider = underlyingCommandProvider;
_commandCollection = new Lazy<CommandCollection>(GetCommandCollectionCore);
}

public CommandCollection GetCommandCollection()
=> _commandCollection.Value;
{
return _cachedCommandCollection ??= GetWrappedCommandCollection(_underlyingCommandProvider.GetCommandCollection());
}

private CommandCollection GetCommandCollectionCore()
private CommandCollection GetWrappedCommandCollection(CommandCollection commandCollection)
{
var commandCollection = _underlyingCommandProvider.GetCommandCollection();
var commands = commandCollection.All;

// If the collection has multiple-commands without primary command, use built-in primary command.
Expand All @@ -44,7 +44,8 @@ private CommandCollection GetCommandCollectionCore()
GetParametersWithBuiltInOptions(command.Options, command.IsPrimaryCommand),
command.Arguments,
command.Overloads,
command.Flags
command.Flags,
(command.SubCommands != null && command.SubCommands != commandCollection) ? GetWrappedCommandCollection(command.SubCommands) : command.SubCommands
);
}

Expand Down
39 changes: 24 additions & 15 deletions src/Cocona.Core/Command/CoconaCommandProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,29 @@ public class CoconaCommandProvider : ICoconaCommandProvider
{
private readonly Type[] _targetTypes;
private static readonly Dictionary<string, List<(MethodInfo Method, CommandOverloadAttribute Attribute)>> _emptyOverloads = new Dictionary<string, List<(MethodInfo Method, CommandOverloadAttribute Attribute)>>();
private readonly Lazy<CommandCollection> _commandCollection;
private readonly bool _treatPublicMethodsAsCommands;
private readonly bool _enableConvertOptionNameToLowerCase;
private readonly bool _enableConvertCommandNameToLowerCase;

public CoconaCommandProvider(Type[] targetTypes, bool treatPublicMethodsAsCommands = true, bool enableConvertOptionNameToLowerCase = false, bool enableConvertCommandNameToLowerCase = false)
{
_targetTypes = targetTypes ?? throw new ArgumentNullException(nameof(targetTypes));
_commandCollection = new Lazy<CommandCollection>(GetCommandCollectionCore, LazyThreadSafetyMode.None);
_treatPublicMethodsAsCommands = treatPublicMethodsAsCommands;
_enableConvertOptionNameToLowerCase = enableConvertOptionNameToLowerCase;
_enableConvertCommandNameToLowerCase = enableConvertCommandNameToLowerCase;
}

public CommandCollection GetCommandCollection()
=> _commandCollection.Value;
=> GetCommandCollectionCore(_targetTypes);

[MethodImpl(MethodImplOptions.NoOptimization)]
private CommandCollection GetCommandCollectionCore()
private CommandCollection GetCommandCollectionCore(IReadOnlyList<Type> targetTypes)
{
var commandMethods = new List<MethodInfo>(10);
var overloadCommandMethods = new Dictionary<string, List<(MethodInfo Method, CommandOverloadAttribute Attribute)>>(10);

// Command types
foreach (var type in _targetTypes)
foreach (var type in targetTypes)
{
if (type.IsAbstract || (type.IsGenericType && type.IsConstructedGenericType)) continue;

Expand All @@ -50,8 +48,8 @@ private CommandCollection GetCommandCollectionCore()
if (method.IsSpecialName || method.DeclaringType == typeof(object)) continue;
if (!_treatPublicMethodsAsCommands && !method.IsPublic) continue;

var (commandAttr, primaryCommandAttr, ignoreAttribute, commandOverloadAttr)
= AttributeHelper.GetAttributes<CommandAttribute, PrimaryCommandAttribute, IgnoreAttribute, CommandOverloadAttribute>(
var (commandAttr, primaryCommandAttr, ignoreAttribute, commandOverloadAttr, subCommandsAttr)
= AttributeHelper.GetAttributes<CommandAttribute, PrimaryCommandAttribute, IgnoreAttribute, CommandOverloadAttribute, SubCommandsAttribute>(
method.GetCustomAttributes(typeof(Attribute), true));

if ((_treatPublicMethodsAsCommands && method.IsPublic) || commandAttr != null || primaryCommandAttr != null)
Expand All @@ -67,10 +65,8 @@ private CommandCollection GetCommandCollectionCore()
}
overloads.Add((method, commandOverloadAttr));
}
else
{
commandMethods.Add(method);
}

commandMethods.Add(method);
}
}
}
Expand Down Expand Up @@ -112,8 +108,8 @@ public CommandDescriptor CreateCommand(MethodInfo methodInfo, bool isSingleComma
ThrowHelper.ArgumentNull(methodInfo, nameof(methodInfo));

// Collect Method attributes
var (commandAttr, primaryCommandAttr, commandHiddenAttr)
= AttributeHelper.GetAttributes<CommandAttribute, PrimaryCommandAttribute, HiddenAttribute>(methodInfo.GetCustomAttributes(typeof(Attribute), true));
var (commandAttr, primaryCommandAttr, commandHiddenAttr, subCommandsAttr)
= AttributeHelper.GetAttributes<CommandAttribute, PrimaryCommandAttribute, HiddenAttribute, SubCommandsAttribute>(methodInfo.GetCustomAttributes(typeof(Attribute), true));

var commandName = commandAttr?.Name ?? methodInfo.Name;
var description = commandAttr?.Description ?? string.Empty;
Expand Down Expand Up @@ -254,8 +250,20 @@ public CommandDescriptor CreateCommand(MethodInfo methodInfo, bool isSingleComma

if (_enableConvertCommandNameToLowerCase) commandName = ToCommandCase(commandName);

// Nested sub commands
var subCommands = default(CommandCollection);
if (subCommandsAttr != null)
{
subCommands = GetCommandCollectionCore(new[] { subCommandsAttr.Type });
if (!string.IsNullOrWhiteSpace(subCommandsAttr.CommandName))
{
commandName = subCommandsAttr.CommandName!;
}
}

var flags = ((isHidden) ? CommandFlags.Hidden : CommandFlags.None) |
((isSingleCommand || isPrimaryCommand) ? CommandFlags.Primary : CommandFlags.None);
((isSingleCommand || isPrimaryCommand) ? CommandFlags.Primary : CommandFlags.None) |
(subCommands != null ? CommandFlags.SubCommandPrimary : CommandFlags.None);

var options = new CommandOptionDescriptor[allOptions.Count];
allOptions.Values.CopyTo(options, 0);
Expand All @@ -269,7 +277,8 @@ public CommandDescriptor CreateCommand(MethodInfo methodInfo, bool isSingleComma
options,
arguments,
overloadDescriptors,
flags
flags,
subCommands
);
}

Expand Down
7 changes: 6 additions & 1 deletion src/Cocona.Core/Command/CommandDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class CommandDescriptor
public IReadOnlyList<CommandArgumentDescriptor> Arguments { get; }
public IReadOnlyList<CommandOverloadDescriptor> Overloads { get; }

public CommandCollection? SubCommands { get; }

public CommandDescriptor(
MethodInfo methodInfo,
string name,
Expand All @@ -35,7 +37,8 @@ public CommandDescriptor(
IReadOnlyList<CommandOptionDescriptor> options,
IReadOnlyList<CommandArgumentDescriptor> arguments,
IReadOnlyList<CommandOverloadDescriptor> overloads,
CommandFlags flags
CommandFlags flags,
CommandCollection? subCommands
)
{
Method = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo));
Expand All @@ -47,6 +50,7 @@ CommandFlags flags
Arguments = arguments ?? throw new ArgumentNullException(nameof(arguments));
Overloads = overloads ?? throw new ArgumentNullException(nameof(overloads));
Flags = flags;
SubCommands = subCommands;
}
}

Expand All @@ -56,5 +60,6 @@ public enum CommandFlags
None = 0,
Primary = 1 << 0,
Hidden = 1 << 1,
SubCommandPrimary = 1 << 2,
}
}
24 changes: 20 additions & 4 deletions src/Cocona.Core/Command/Dispatcher/CoconaCommandDispatcher.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Cocona.Application;
using Cocona.CommandLine;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Cocona.Command.Features;

namespace Cocona.Command.Dispatcher
{
Expand Down Expand Up @@ -44,7 +46,9 @@ public async ValueTask<int> DispatchAsync(CancellationToken cancellationToken)
{
var commandCollection = _commandProvider.GetCommandCollection();
var args = _commandLineArgumentProvider.GetArguments();
var subCommandStack = new List<CommandDescriptor>();

Retry:
var matchedCommand = default(CommandDescriptor);
if (commandCollection.All.Count > 1)
{
Expand All @@ -62,6 +66,14 @@ public async ValueTask<int> DispatchAsync(CancellationToken cancellationToken)

// NOTE: Skip a first argument that is command name.
args = args.Skip(1).ToArray();

// If the command have nested sub-commands, try to restart parse command.
if (matchedCommand.SubCommands != null)
{
commandCollection = matchedCommand.SubCommands;
subCommandStack.Add(matchedCommand);
goto Retry;
}
}
else
{
Expand Down Expand Up @@ -92,14 +104,18 @@ public async ValueTask<int> DispatchAsync(CancellationToken cancellationToken)
var parsedCommandLine = _commandLineParser.ParseCommand(args, matchedCommand.Options, matchedCommand.Arguments);
var dispatchAsync = _dispatcherPipelineBuilder.Build();

// Activate a command type.
var commandInstance = _activator.GetServiceOrCreateInstance(_serviceProvider, matchedCommand.CommandType);
if (commandInstance == null) throw new InvalidOperationException($"Unable to activate command type '{matchedCommand.CommandType.FullName}'");

// Set CoconaAppContext
_appContext.Current = new CoconaAppContext(cancellationToken);
_appContext.Current = new CoconaAppContext(matchedCommand, cancellationToken);
_appContext.Current.Features.Set<ICoconaCommandFeature>(new CoconaCommandFeature(commandCollection, matchedCommand, subCommandStack, commandInstance));

// Dispatch command.
var commandInstance = _activator.GetServiceOrCreateInstance(_serviceProvider, matchedCommand.CommandType);
// Dispatch the command
try
{
var ctx = new CommandDispatchContext(matchedCommand, parsedCommandLine, commandInstance!, cancellationToken);
var ctx = new CommandDispatchContext(matchedCommand, parsedCommandLine, commandInstance, cancellationToken);
return await dispatchAsync(ctx);
}
finally
Expand Down
30 changes: 30 additions & 0 deletions src/Cocona.Core/Command/Features/CoconaCommandFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Cocona.Command.Features
{
public interface ICoconaCommandFeature
{
object CommandInstance { get; }
CommandDescriptor Command { get; }
CommandCollection CommandCollection { get; }
IReadOnlyList<CommandDescriptor> CommandStack { get; }
}

public class CoconaCommandFeature : ICoconaCommandFeature
{
public object CommandInstance { get; }
public CommandDescriptor Command { get; }
public CommandCollection CommandCollection { get; }
public IReadOnlyList<CommandDescriptor> CommandStack { get; }

public CoconaCommandFeature(CommandCollection commandCollection, CommandDescriptor commandDescriptor, IReadOnlyList<CommandDescriptor> commandStack, object commandInstance)
{
CommandCollection = commandCollection ?? throw new ArgumentNullException(nameof(commandCollection));
Command = commandDescriptor ?? throw new ArgumentNullException(nameof(commandDescriptor));
CommandInstance = commandInstance ?? throw new ArgumentNullException(nameof(commandInstance));
CommandStack = commandStack ?? throw new ArgumentNullException(nameof(commandStack));
}
}
}
Loading

0 comments on commit 2569752

Please sign in to comment.