From 7615ed0e2e99707ae21570ac812c2bd94591473a Mon Sep 17 00:00:00 2001 From: Jonathan Berube Date: Fri, 20 May 2016 13:51:37 -0700 Subject: [PATCH] Added the ability to declare subcommands. (#2) * Added the ability to declare subcommands. * Added logic to print subcommand help. --- src/ThorNet.Terminal/project.json | 4 +- src/ThorNet.UnitTests/SubcommandTests.cs | 58 ++++++++++++++++ src/ThorNet.UnitTests/project.json | 4 +- src/ThorNet/Thor.cs | 87 ++++++++++++++++++++---- src/ThorNet/project.json | 2 +- 5 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 src/ThorNet.UnitTests/SubcommandTests.cs diff --git a/src/ThorNet.Terminal/project.json b/src/ThorNet.Terminal/project.json index 76f6d20..7bdc3c0 100644 --- a/src/ThorNet.Terminal/project.json +++ b/src/ThorNet.Terminal/project.json @@ -11,7 +11,7 @@ "type": "platform", "version": "1.0.0-rc2-3002702" }, - "ThorNet": "0.2.1" + "ThorNet": "0.3.0" }, "frameworks": { "netcoreapp1.0": { @@ -21,5 +21,5 @@ "tooling": { "defaultNamespace": "ThorNet.Terminal" }, - "version": "0.2.1" + "version": "0.3.0" } diff --git a/src/ThorNet.UnitTests/SubcommandTests.cs b/src/ThorNet.UnitTests/SubcommandTests.cs new file mode 100644 index 0000000..76a1501 --- /dev/null +++ b/src/ThorNet.UnitTests/SubcommandTests.cs @@ -0,0 +1,58 @@ +using System; +using Xunit; + +namespace ThorNet.UnitTests +{ + /// + /// Tests to make sure subcommands are run. + /// + public class SubcommandTests + { + [Fact] + public void ConflictingCommandAndSubcommand_Throws() + { + Assert.Throws(() => + { + new DuplicateTarget(); + }); + } + + public class DuplicateTarget : Thor + { + public DuplicateTarget() + { + Subcommand(); + } + + public void Duplicate() { } + } + + public class Duplicate : Thor + { + } + + [Fact] + public void Subcommand_IsTriggered() + { + Assert.Equal(0, Trigger.Counter); + Thor.Start(new[] { nameof(Trigger), nameof(Trigger.Increment) }); + Assert.Equal(1, Trigger.Counter); + } + + public class TriggerTarget : Thor + { + public TriggerTarget() + { + Subcommand(); + } + } + + public class Trigger : Thor + { + public static int Counter => _counter; + static int _counter; + + public void Increment() { _counter++; } + } + } +} diff --git a/src/ThorNet.UnitTests/project.json b/src/ThorNet.UnitTests/project.json index 035fe46..a2fff38 100644 --- a/src/ThorNet.UnitTests/project.json +++ b/src/ThorNet.UnitTests/project.json @@ -2,7 +2,7 @@ "authors": [ "joncloud" ], "dependencies": { "dotnet-test-xunit": "1.0.0-rc2-build10015", - "ThorNet": "0.2.1", + "ThorNet": "0.3.0", "xunit": "2.1.0" }, "description": "Thor .NET Unit Tests", @@ -24,5 +24,5 @@ "tooling": { "defaultNamespace": "ThorNet.UnitTests" }, - "version": "0.2.1" + "version": "0.3.0" } diff --git a/src/ThorNet/Thor.cs b/src/ThorNet/Thor.cs index 3fc4d97..c772e4d 100644 --- a/src/ThorNet/Thor.cs +++ b/src/ThorNet/Thor.cs @@ -8,11 +8,13 @@ public class Thor : IThor { private Dictionary _commands; private Dictionary> _options; + private Dictionary> _subCommands; public Thor() : this(new ConsoleWrapper()) { _commands = LoadCommands(); _options = new Dictionary>(); + _subCommands = new Dictionary>(); } public Thor(ITerminal terminal) { @@ -65,7 +67,7 @@ bool IThor.HasOption(string name) { } [Desc("help [COMMAND]", "Describe available commands or one specific command")] - public void help(string commandName = null) { + public void help(string commandName = null, string subcommandName = null) { string name = GetPackageName(); // Print all of the commands. @@ -78,6 +80,11 @@ public void help(string commandName = null) { } Terminal.WriteLine(message); } + + foreach (var thor in _subCommands.Values.Select(factory => factory())) + { + thor.help(); + } } // Print a specific command. @@ -100,8 +107,18 @@ public void help(string commandName = null) { Terminal.WriteLine(command.Description); } - else { - Terminal.WriteLine($"Could not find command \"{commandName}\"."); + else + { + Func factory; + if (_subCommands.TryGetValue(commandName, out factory)) + { + var thor = factory(); + thor.help(subcommandName); + } + else + { + Terminal.WriteLine($"Could not find command \"{commandName}\"."); + } } } } @@ -123,7 +140,18 @@ internal void Invoke(string commandName, string[] args) { command.Invoke(args); } else { - Terminal.WriteLine($"Could not find command \"{commandName}\"."); + Func subCommand; + if (_subCommands.TryGetValue(commandName, out subCommand)) + { + commandName = PrepareInvocationArguments(ref args); + + subCommand().Invoke(commandName, args); + } + + else + { + Terminal.WriteLine($"Could not find command \"{commandName}\"."); + } } } @@ -177,7 +205,7 @@ protected IEnumerable Options(string name) { } /// - /// Gets all options provided byn name converted to the type specified. + /// Gets all options provided by name converted to the type specified. /// /// The name of the option. /// The delegate used for converting the text value to the type value. @@ -201,27 +229,58 @@ protected IEnumerable Options(string name, Func convert = null, } } } - + /// - /// Starts the thor program. + /// Prepares the input arguments to invoke /// - /// The arguments given from the command line. - public static void Start(string[] args) - where T : Thor, new() { - + /// The array of arguments provided. This is modified to remove the first argument if present. + /// The name of the command to invoke. If no arguments are provided, this defaults to . + internal static string PrepareInvocationArguments(ref string[] args) + { string commandName; - + // Default to help. - if (!args.Any()) { + if (!args.Any()) + { commandName = nameof(help); } - else { + else + { commandName = args[0]; args = args.Skip(1).ToArray(); } + + return commandName; + } + + /// + /// Starts the thor program. + /// + /// The arguments given from the command line. + public static void Start(string[] args) + where T : Thor, new() { + + string commandName = PrepareInvocationArguments(ref args); T thor = new T(); thor.Invoke(commandName, args); } + + /// + /// Associates a subcommand with this command. + /// + /// The type of class to use for a subcommand. + /// The optional name of the subcommand. If no name is provided, then the name of the class is used. + protected void Subcommand(string name = null) + where T : Thor, new() + { + name = name ?? typeof(T).Name; + if (_commands.ContainsKey(name)) + { + throw new ArgumentOutOfRangeException(nameof(name), $"{name} is a command, and cannot also be a subcommand."); + } + + _subCommands.Add(name ?? typeof(T).Name, () => new T()); + } } } \ No newline at end of file diff --git a/src/ThorNet/project.json b/src/ThorNet/project.json index 5671b31..00d21e3 100644 --- a/src/ThorNet/project.json +++ b/src/ThorNet/project.json @@ -30,5 +30,5 @@ "tooling": { "defaultNamespace": "ThorNet" }, - "version": "0.2.1" + "version": "0.3.0" }