diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
index 69f042ac3..1de1757f1 100644
--- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
+++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
@@ -43,6 +43,9 @@
CommandBuilder.cs
+
+ CommandMap.cs
+
CommandParser.cs
diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs
index 2f4978566..50f958ba2 100644
--- a/src/Discord.Net.Commands/Command.cs
+++ b/src/Discord.Net.Commands/Command.cs
@@ -1,23 +1,102 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace Discord.Commands
{
+ public sealed class CommandParameter
+ {
+ public string Name { get; }
+ public bool IsOptional { get; }
+ public bool IsCatchAll { get; }
+
+ public CommandParameter(string name, bool isOptional, bool isCatchAll)
+ {
+ Name = name;
+ IsOptional = isOptional;
+ IsCatchAll = isCatchAll;
+ }
+ }
+
public sealed class Command
{
public string Text { get; }
- public int? MinArgs { get; internal set; }
- public int? MaxArgs { get; internal set; }
+ public int? MinArgs { get; private set; }
+ public int? MaxArgs { get; private set; }
public int MinPerms { get; internal set; }
public bool IsHidden { get; internal set; }
public string Description { get; internal set; }
- internal Func Handler;
+
+ public IEnumerable Aliases => _aliases;
+ private string[] _aliases;
+
+ public IEnumerable Parameters => _parameters;
+ private CommandParameter[] _parameters;
+
+ private Func _handler;
internal Command(string text)
{
Text = text;
- IsHidden = false; // Set false by default to avoid null error
- Description = "No description set for this command.";
+ IsHidden = false;
+ _aliases = new string[0];
+ _parameters = new CommandParameter[0];
+ }
+
+ internal void SetAliases(string[] aliases)
+ {
+ _aliases = aliases;
+ }
+
+ internal void SetParameters(CommandParameter[] parameters)
+ {
+ _parameters = parameters;
+ if (parameters != null)
+ {
+ if (parameters.Length == 0)
+ {
+ MinArgs = 0;
+ MaxArgs = 0;
+ }
+ else
+ {
+ if (parameters[parameters.Length - 1].IsCatchAll)
+ MaxArgs = null;
+ else
+ MaxArgs = parameters.Length;
+
+ int? optionalStart = null;
+ for (int i = parameters.Length - 1; i >= 0; i--)
+ {
+ if (parameters[i].IsOptional)
+ optionalStart = i;
+ else
+ break;
+ }
+ if (optionalStart == null)
+ MinArgs = MaxArgs;
+ else
+ MinArgs = optionalStart.Value;
+ }
+ }
+ }
+
+ internal void SetHandler(Func func)
+ {
+ _handler = func;
+ }
+ internal void SetHandler(Action func)
+ {
+ _handler = e => { func(e); return TaskHelper.CompletedTask; };
+ }
+
+ internal Task Run(CommandEventArgs args)
+ {
+ var task = _handler(args);
+ if (task != null)
+ return task;
+ else
+ return TaskHelper.CompletedTask;
}
}
}
diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs
index 13e3ff89f..981bc23df 100644
--- a/src/Discord.Net.Commands/CommandBuilder.cs
+++ b/src/Discord.Net.Commands/CommandBuilder.cs
@@ -1,50 +1,55 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace Discord.Commands
{
public sealed class CommandBuilder
{
+ private readonly CommandsPlugin _plugin;
private readonly Command _command;
- public CommandBuilder(Command command)
+ private List _params;
+ private bool _hasOptional, _hasCatchAll;
+ private string _prefix;
+
+ public CommandBuilder(CommandsPlugin plugin, Command command, string prefix)
{
+ _plugin = plugin;
_command = command;
- }
-
- public CommandBuilder ArgsEqual(int argCount)
- {
- _command.MinArgs = argCount;
- _command.MaxArgs = argCount;
- return this;
- }
- public CommandBuilder ArgsAtLeast(int minArgCount)
- {
- _command.MinArgs = minArgCount;
- _command.MaxArgs = null;
- return this;
- }
- public CommandBuilder ArgsAtMost(int maxArgCount)
+ _params = new List();
+ _prefix = prefix;
+ }
+
+ public CommandBuilder Alias(params string[] aliases)
{
- _command.MinArgs = null;
- _command.MaxArgs = maxArgCount;
+ aliases = aliases.Select(x => AppendPrefix(_prefix, x)).ToArray();
+ _command.SetAliases(aliases);
return this;
}
- public CommandBuilder ArgsBetween(int minArgCount, int maxArgCount)
+ public CommandBuilder Info(string description)
{
- _command.MinArgs = minArgCount;
- _command.MaxArgs = maxArgCount;
+ _command.Description = description;
return this;
}
- public CommandBuilder NoArgs()
+ public CommandBuilder Parameter(string name, bool isOptional = false, bool isCatchAll = false)
{
- _command.MinArgs = 0;
- _command.MaxArgs = 0;
+ if (_hasCatchAll)
+ throw new Exception("No parameters may be added after the catch-all");
+ if (_hasOptional && isOptional)
+ throw new Exception("Non-optional parameters may not be added after an optional one");
+
+ _params.Add(new CommandParameter(name, isOptional, isCatchAll));
+
+ if (isOptional)
+ _hasOptional = true;
+ if (isCatchAll)
+ _hasCatchAll = true;
return this;
}
- public CommandBuilder AnyArgs()
+ public CommandBuilder IsHidden()
{
- _command.MinArgs = null;
- _command.MaxArgs = null;
+ _command.IsHidden = true;
return this;
}
@@ -54,32 +59,45 @@ namespace Discord.Commands
return this;
}
- public CommandBuilder Desc(string desc)
- {
- _command.Description = desc;
- return this;
- }
-
- public CommandBuilder IsHidden()
- {
- _command.IsHidden = true;
- return this;
- }
-
- public CommandBuilder Do(Func func)
+ public void Do(Func func)
{
- _command.Handler = func;
- return this;
+ _command.SetHandler(func);
+ Build();
}
- public CommandBuilder Do(Action func)
+ public void Do(Action func)
{
- _command.Handler = e => { func(e); return TaskHelper.CompletedTask; };
- return this;
+ _command.SetHandler(func);
+ Build();
+ }
+ private void Build()
+ {
+ _command.SetParameters(_params.ToArray());
+ foreach (var alias in _command.Aliases)
+ _plugin.Map.AddCommand(alias, _command);
+ _plugin.AddCommand(_command);
+ }
+
+ internal static string AppendPrefix(string prefix, string cmd)
+ {
+ if (cmd != "")
+ {
+ if (prefix != "")
+ return prefix + ' ' + cmd;
+ else
+ return cmd;
+ }
+ else
+ {
+ if (prefix != "")
+ return prefix;
+ else
+ throw new ArgumentOutOfRangeException(nameof(cmd));
+ }
}
}
public sealed class CommandGroupBuilder
{
- private readonly CommandsPlugin _plugin;
+ internal readonly CommandsPlugin _plugin;
private readonly string _prefix;
private int _defaultMinPermissions;
@@ -104,25 +122,9 @@ namespace Discord.Commands
=> CreateCommand("");
public CommandBuilder CreateCommand(string cmd)
{
- string text;
- if (cmd != "")
- {
- if (_prefix != "")
- text = _prefix + ' ' + cmd;
- else
- text = cmd;
- }
- else
- {
- if (_prefix != "")
- text = _prefix;
- else
- throw new ArgumentOutOfRangeException(nameof(cmd));
- }
- var command = new Command(text);
+ var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd));
command.MinPerms = _defaultMinPermissions;
- _plugin.AddCommand(command);
- return new CommandBuilder(command);
+ return new CommandBuilder(_plugin, command, _prefix);
}
}
}
diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs
new file mode 100644
index 000000000..504479400
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandMap.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+
+namespace Discord.Commands
+{
+ internal class CommandMap
+ {
+ private CommandMap _parent;
+ private Command _command;
+ private readonly Dictionary _subCommands;
+
+ public CommandMap(CommandMap parent)
+ {
+ _parent = parent;
+ _subCommands = new Dictionary();
+ }
+
+ public CommandMap GetMap(string text)
+ {
+ CommandMap map;
+ if (_subCommands.TryGetValue(text, out map))
+ return map;
+ else
+ return null;
+ }
+
+ public Command GetCommand()
+ {
+ if (_command != null)
+ return _command;
+ else if (_parent != null)
+ return _parent.GetCommand();
+ else
+ return null;
+ }
+ public Command GetCommand(string text)
+ {
+ return GetCommand(0, text.Split(' '));
+ }
+ public Command GetCommand(int index, string[] parts)
+ {
+ if (index != parts.Length)
+ {
+ string nextPart = parts[index];
+ CommandMap nextGroup;
+ if (_subCommands.TryGetValue(nextPart, out nextGroup))
+ {
+ var cmd = nextGroup.GetCommand(index + 1, parts);
+ if (cmd != null)
+ return cmd;
+ }
+ }
+
+ if (_command != null)
+ return _command;
+ return null;
+ }
+
+ public void AddCommand(string text, Command command)
+ {
+ AddCommand(0, text.Split(' '), command);
+ }
+ public void AddCommand(int index, string[] parts, Command command)
+ {
+ if (index != parts.Length)
+ {
+ string nextPart = parts[index];
+ CommandMap nextGroup;
+ if (!_subCommands.TryGetValue(nextPart, out nextGroup))
+ {
+ nextGroup = new CommandMap(this);
+ _subCommands.Add(nextPart, nextGroup);
+ }
+ nextGroup.AddCommand(index + 1, parts, command);
+ }
+ else
+ {
+ if (_command != null)
+ throw new InvalidOperationException("A command has already been added with this path.");
+ _command = command;
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs
index 76a4b4006..08241eb1b 100644
--- a/src/Discord.Net.Commands/CommandParser.cs
+++ b/src/Discord.Net.Commands/CommandParser.cs
@@ -15,37 +15,69 @@ namespace Discord.Commands
}
//TODO: Check support for escaping
- public static class CommandParser
+ internal static class CommandParser
{
private enum CommandParserPart
{
None,
- CommandName,
Parameter,
QuotedParameter,
DoubleQuotedParameter
}
- public static bool Parse(string input, out string command, out CommandPart[] args)
+ public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos)
{
- return Parse(input, out command, out args, true);
- }
- public static bool ParseArgs(string input, out CommandPart[] args)
- {
- string ignored;
- return Parse(input, out ignored, out args, false);
+ int startPosition = 0;
+ int endPosition = 0;
+ int inputLength = input.Length;
+ bool isEscaped = false;
+ command = null;
+ endPos = 0;
+
+ if (input == "")
+ return false;
+
+ while (endPosition < inputLength)
+ {
+ char currentChar = input[endPosition++];
+ if (isEscaped)
+ isEscaped = false;
+ else if (currentChar == '\\')
+ isEscaped = true;
+
+ if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
+ {
+ int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
+ string temp = input.Substring(startPosition, length);
+ if (temp == "")
+ startPosition = endPosition;
+ else
+ {
+ var newMap = map.GetMap(temp);
+ if (newMap != null)
+ {
+ map = newMap;
+ endPos = endPosition;
+ }
+ else
+ break;
+ startPosition = endPosition;
+ }
+ }
+ }
+ command = map.GetCommand(); //Work our way backwards to find a command that matches our input
+ return command != null;
}
- private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand)
+ public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args)
{
- CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None;
- int startPosition = 0;
- int endPosition = 0;
+ CommandParserPart currentPart = CommandParserPart.None;
+ int startPosition = startPos;
+ int endPosition = startPos;
int inputLength = input.Length;
bool isEscaped = false;
List argList = new List();
-
- command = null;
+
args = null;
if (input == "")
@@ -61,21 +93,6 @@ namespace Discord.Commands
switch (currentPart)
{
- case CommandParserPart.CommandName:
- if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
- {
- int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
- string temp = input.Substring(startPosition, length);
- if (temp == "")
- startPosition = endPosition;
- else
- {
- currentPart = CommandParserPart.None;
- command = temp;
- startPosition = endPosition;
- }
- }
- break;
case CommandParserPart.None:
if ((!isEscaped && currentChar == '\"'))
{
@@ -126,9 +143,6 @@ namespace Discord.Commands
}
}
- if (parseCommand && (command == null || command == ""))
- return false;
-
args = argList.ToArray();
return true;
}
diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs
index 0b3f4ee14..da31fa6f7 100644
--- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs
+++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs
@@ -22,7 +22,7 @@ namespace Discord.Commands
}
}
- public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount }
+ public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput }
public class CommandErrorEventArgs : CommandEventArgs
{
public CommandErrorType ErrorType { get; }
diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs
index 7f7149b95..f15664348 100644
--- a/src/Discord.Net.Commands/CommandsPlugin.cs
+++ b/src/Discord.Net.Commands/CommandsPlugin.cs
@@ -10,29 +10,30 @@ namespace Discord.Commands
public partial class CommandsPlugin
{
private readonly DiscordClient _client;
- private Func _getPermissions;
-
- private Dictionary _commands;
+ private readonly Func _getPermissions;
- public Dictionary Commands => _commands;
+ public IEnumerable Commands => _commands;
+ private readonly List _commands;
+
+ internal CommandMap Map => _map;
+ private readonly CommandMap _map;
- public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure.
- public List CommandChars { get; set; }
- public bool UseCommandChar { get; set; }
+ public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } }
+ private char[] _commandChars;
+
public bool RequireCommandCharInPublic { get; set; }
public bool RequireCommandCharInPrivate { get; set; }
public bool HelpInPublic { get; set; }
public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false)
{
- _client = client; // Wait why is this even set
+ _client = client;
_getPermissions = getPermissions;
- _commands = new Dictionary();
+ _commands = new List();
+ _map = new CommandMap(null);
- CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary.
- CommandChars = new List { '!', '?', '/' };
- UseCommandChar = true;
+ _commandChars = new char[] { '!' };
RequireCommandCharInPublic = true;
RequireCommandCharInPrivate = true;
HelpInPublic = true;
@@ -40,9 +41,9 @@ namespace Discord.Commands
if (builtInHelp)
{
CreateCommand("help")
- .ArgsBetween(0, 1)
+ .Parameter("command", isOptional: true)
.IsHidden()
- .Desc("Returns information about commands.")
+ .Info("Returns information about commands.")
.Do(async e =>
{
if (e.Command.Text != "help")
@@ -50,29 +51,18 @@ namespace Discord.Commands
else
{
if (e.Args == null)
- {
- StringBuilder output = new StringBuilder();
- bool first = true;
+ {
+ int permissions = getPermissions(e.User);
+ StringBuilder output = new StringBuilder();
output.AppendLine("These are the commands you can use:");
output.Append("`");
- int permissions = getPermissions(e.User);
- foreach (KeyValuePair k in _commands)
- {
- if (permissions >= k.Value.MinPerms && !k.Value.IsHidden)
- if (first)
- {
- output.Append(k.Key);
- first = false;
- }
- else
- output.Append($", {k.Key}");
- }
+ output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden)));
output.Append("`");
- if (CommandChars.Count == 1)
- output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command.");
+ if (_commandChars.Length == 1)
+ output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command.");
else
- output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command.");
+ output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command.");
output.AppendLine("`help ` can tell you more about how to use a command.");
@@ -80,8 +70,9 @@ namespace Discord.Commands
}
else
{
- if (_commands.ContainsKey(e.Args[0]))
- await Reply(e, CommandDetails(_commands[e.Args[0]]));
+ var cmd = _map.GetCommand(e.Args[0]);
+ if (cmd != null)
+ await Reply(e, CommandDetails(cmd));
else
await Reply(e, $"`{e.Args[0]}` is not a valid command.");
}
@@ -99,7 +90,7 @@ namespace Discord.Commands
string msg = e.Message.Text;
if (msg.Length == 0) return;
- if (UseCommandChar)
+ if (_commandChars.Length > 0)
{
bool isPrivate = e.Message.Channel.IsPrivate;
bool hasCommandChar = CommandChars.Contains(msg[0]);
@@ -112,44 +103,41 @@ namespace Discord.Commands
return; // Same, but public.
}
- string cmd;
- CommandPart[] args;
- if (!CommandParser.Parse(msg, out cmd, out args))
- return;
-
- if (_commands.ContainsKey(cmd))
- {
- Command comm = _commands[cmd];
-
- //Clean args
+ //Parse command
+ Command command;
+ int argPos;
+ CommandParser.ParseCommand(msg, _map, out command, out argPos);
+ if (command == null)
+ {
+ CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null);
+ RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs);
+ return;
+ }
+ else
+ {
+ //Parse arguments
+ CommandPart[] args;
+ if (!CommandParser.ParseArgs(msg, argPos, command, out args))
+ {
+ CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null);
+ RaiseCommandError(CommandErrorType.InvalidInput, errorArgs);
+ return;
+ }
int argCount = args.Length;
- string[] newArgs = null;
-
- if (comm.MaxArgs != null && argCount > 0)
- {
- newArgs = new string[(int)comm.MaxArgs];
- for (int j = 0; j < newArgs.Length; j++)
- newArgs[j] = args[j].Value;
- }
- else if (comm.MaxArgs == null && comm.MinArgs == null)
- {
- newArgs = new string[argCount];
- for (int j = 0; j < newArgs.Length; j++)
- newArgs[j] = args[j].Value;
- }
+ //Get information for the rest of the steps
int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0;
- var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs);
+ var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray());
// Check permissions
- if (userPermissions < comm.MinPerms)
+ if (userPermissions < command.MinPerms)
{
RaiseCommandError(CommandErrorType.BadPermissions, eventArgs);
return;
}
-
+
//Check arg count
- if (argCount < comm.MinArgs)
+ if (argCount < command.MinArgs)
{
RaiseCommandError(CommandErrorType.BadArgCount, eventArgs);
return;
@@ -159,21 +147,13 @@ namespace Discord.Commands
try
{
RaiseRanCommand(eventArgs);
- var task = comm.Handler(eventArgs);
- if (task != null)
- await task.ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- RaiseCommandError(CommandErrorType.Exception, eventArgs, ex);
- }
- }
- else
- {
- CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null);
- RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs);
- return;
- }
+ await command.Run(eventArgs).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ RaiseCommandError(CommandErrorType.Exception, eventArgs, ex);
+ }
+ }
};
}
@@ -198,7 +178,7 @@ namespace Discord.Commands
else if (command.MinArgs == null && command.MaxArgs != null)
output.Append($" ≤{command.MaxArgs.ToString()} Args");
- output.Append($": {command.Description}");
+ output.Append($": {command.Description ?? "No description set for this command."}");
return output.ToString();
}
@@ -216,13 +196,14 @@ namespace Discord.Commands
public CommandBuilder CreateCommand(string cmd)
{
var command = new Command(cmd);
- _commands.Add(cmd, command);
- return new CommandBuilder(command);
+ _commands.Add(command);
+ return new CommandBuilder(null, command, "");
}
internal void AddCommand(Command command)
{
- _commands.Add(command.Text, command);
+ _commands.Add(command);
+ _map.AddCommand(command.Text, command);
}
}
}