Skip to content

Commit

Permalink
Added the ability to declare subcommands. (#2)
Browse files Browse the repository at this point in the history
* Added the ability to declare subcommands.

* Added logic to print subcommand help.
  • Loading branch information
joncloud committed May 20, 2016
1 parent da62952 commit 7615ed0
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 19 deletions.
4 changes: 2 additions & 2 deletions src/ThorNet.Terminal/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"type": "platform",
"version": "1.0.0-rc2-3002702"
},
"ThorNet": "0.2.1"
"ThorNet": "0.3.0"
},
"frameworks": {
"netcoreapp1.0": {
Expand All @@ -21,5 +21,5 @@
"tooling": {
"defaultNamespace": "ThorNet.Terminal"
},
"version": "0.2.1"
"version": "0.3.0"
}
58 changes: 58 additions & 0 deletions src/ThorNet.UnitTests/SubcommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using Xunit;

namespace ThorNet.UnitTests
{
/// <summary>
/// Tests to make sure subcommands are run.
/// </summary>
public class SubcommandTests
{
[Fact]
public void ConflictingCommandAndSubcommand_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
new DuplicateTarget();
});
}

public class DuplicateTarget : Thor
{
public DuplicateTarget()
{
Subcommand<Duplicate>();
}

public void Duplicate() { }
}

public class Duplicate : Thor
{
}

[Fact]
public void Subcommand_IsTriggered()
{
Assert.Equal(0, Trigger.Counter);
Thor.Start<TriggerTarget>(new[] { nameof(Trigger), nameof(Trigger.Increment) });
Assert.Equal(1, Trigger.Counter);
}

public class TriggerTarget : Thor
{
public TriggerTarget()
{
Subcommand<Trigger>();
}
}

public class Trigger : Thor
{
public static int Counter => _counter;
static int _counter;

public void Increment() { _counter++; }
}
}
}
4 changes: 2 additions & 2 deletions src/ThorNet.UnitTests/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -24,5 +24,5 @@
"tooling": {
"defaultNamespace": "ThorNet.UnitTests"
},
"version": "0.2.1"
"version": "0.3.0"
}
87 changes: 73 additions & 14 deletions src/ThorNet/Thor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ public class Thor : IThor {

private Dictionary<string, ThorCommand> _commands;
private Dictionary<string, List<string>> _options;
private Dictionary<string, Func<Thor>> _subCommands;

public Thor()
: this(new ConsoleWrapper()) {
_commands = LoadCommands();
_options = new Dictionary<string, List<string>>();
_subCommands = new Dictionary<string, Func<Thor>>();
}

public Thor(ITerminal terminal) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -100,8 +107,18 @@ public void help(string commandName = null) {

Terminal.WriteLine(command.Description);
}
else {
Terminal.WriteLine($"Could not find command \"{commandName}\".");
else
{
Func<Thor> factory;
if (_subCommands.TryGetValue(commandName, out factory))
{
var thor = factory();
thor.help(subcommandName);
}
else
{
Terminal.WriteLine($"Could not find command \"{commandName}\".");
}
}
}
}
Expand All @@ -123,7 +140,18 @@ internal void Invoke(string commandName, string[] args) {
command.Invoke(args);
}
else {
Terminal.WriteLine($"Could not find command \"{commandName}\".");
Func<Thor> subCommand;
if (_subCommands.TryGetValue(commandName, out subCommand))
{
commandName = PrepareInvocationArguments(ref args);

subCommand().Invoke(commandName, args);
}

else
{
Terminal.WriteLine($"Could not find command \"{commandName}\".");
}
}
}

Expand Down Expand Up @@ -177,7 +205,7 @@ protected IEnumerable<string> Options(string name) {
}

/// <summary>
/// Gets all options provided byn name converted to the type specified.
/// Gets all options provided by name converted to the type specified.
/// </summary>
/// <param name="name">The name of the option.</param>
/// <param name="convert">The delegate used for converting the text value to the type value.</param>
Expand All @@ -201,27 +229,58 @@ protected IEnumerable<T> Options<T>(string name, Func<string, T> convert = null,
}
}
}

/// <summary>
/// Starts the thor program.
/// Prepares the input arguments to invoke <see cref="Thor.Invoke(string, string[])"/>
/// </summary>
/// <param name="args">The arguments given from the command line.</param>
public static void Start<T>(string[] args)
where T : Thor, new() {

/// <param name="args">The array of arguments provided. This is modified to remove the first argument if present.</param>
/// <returns>The name of the command to invoke. If no arguments are provided, this defaults to <see cref="help(string)"/>.</returns>
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;
}

/// <summary>
/// Starts the thor program.
/// </summary>
/// <param name="args">The arguments given from the command line.</param>
public static void Start<T>(string[] args)
where T : Thor, new() {

string commandName = PrepareInvocationArguments(ref args);

T thor = new T();
thor.Invoke(commandName, args);
}

/// <summary>
/// Associates a subcommand with this command.
/// </summary>
/// <typeparam name="T">The type of class to use for a subcommand.</typeparam>
/// <param name="name">The optional name of the subcommand. If no name is provided, then the name of the class is used.</param>
protected void Subcommand<T>(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());
}
}
}
2 changes: 1 addition & 1 deletion src/ThorNet/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@
"tooling": {
"defaultNamespace": "ThorNet"
},
"version": "0.2.1"
"version": "0.3.0"
}

0 comments on commit 7615ed0

Please sign in to comment.