You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
7.9 KiB
199 lines
7.9 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Discord.Commands
|
|
{
|
|
internal static class CommandParser
|
|
{
|
|
private enum ParserPart
|
|
{
|
|
None,
|
|
Parameter,
|
|
QuotedParameter
|
|
}
|
|
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap)
|
|
{
|
|
ParameterInfo curParam = null;
|
|
StringBuilder argBuilder = new StringBuilder(input.Length);
|
|
int endPos = input.Length;
|
|
var curPart = ParserPart.None;
|
|
int lastArgEndPos = int.MinValue;
|
|
var argList = ImmutableArray.CreateBuilder<TypeReaderResult>();
|
|
var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>();
|
|
bool isEscaping = false;
|
|
char c, matchQuote = '\0';
|
|
|
|
// local helper functions
|
|
bool IsOpenQuote(IReadOnlyDictionary<char, char> dict, char ch)
|
|
{
|
|
// return if the key is contained in the dictionary if it is populated
|
|
if (dict.Count != 0)
|
|
return dict.ContainsKey(ch);
|
|
// or otherwise if it is the default double quote
|
|
return c == '\"';
|
|
}
|
|
|
|
char GetMatch(IReadOnlyDictionary<char, char> dict, char ch)
|
|
{
|
|
// get the corresponding value for the key, if it exists
|
|
// and if the dictionary is populated
|
|
if (dict.Count != 0 && dict.TryGetValue(c, out var value))
|
|
return value;
|
|
// or get the default pair of the default double quote
|
|
return '\"';
|
|
}
|
|
|
|
for (int curPos = startPos; curPos <= endPos; curPos++)
|
|
{
|
|
if (curPos < endPos)
|
|
c = input[curPos];
|
|
else
|
|
c = '\0';
|
|
|
|
//If we're processing an remainder parameter, ignore all other logic
|
|
if (curParam != null && curParam.IsRemainder && curPos != endPos)
|
|
{
|
|
argBuilder.Append(c);
|
|
continue;
|
|
}
|
|
|
|
//If this character is escaped, skip it
|
|
if (isEscaping)
|
|
{
|
|
if (curPos != endPos)
|
|
{
|
|
// if this character matches the quotation mark of the end of the string
|
|
// means that it should be escaped
|
|
// but if is not, then there is no reason to escape it then
|
|
if (c != matchQuote)
|
|
{
|
|
// if no reason to escape the next character, then re-add \ to the arg
|
|
argBuilder.Append('\\');
|
|
}
|
|
|
|
argBuilder.Append(c);
|
|
isEscaping = false;
|
|
continue;
|
|
}
|
|
}
|
|
//Are we escaping the next character?
|
|
if (c == '\\' && (curParam == null || !curParam.IsRemainder))
|
|
{
|
|
isEscaping = true;
|
|
continue;
|
|
}
|
|
|
|
//If we're not currently processing one, are we starting the next argument yet?
|
|
if (curPart == ParserPart.None)
|
|
{
|
|
if (char.IsWhiteSpace(c) || curPos == endPos)
|
|
continue; //Skip whitespace between arguments
|
|
else if (curPos == lastArgEndPos)
|
|
return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments.");
|
|
else
|
|
{
|
|
if (curParam == null)
|
|
curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null;
|
|
|
|
if (curParam != null && curParam.IsRemainder)
|
|
{
|
|
argBuilder.Append(c);
|
|
continue;
|
|
}
|
|
|
|
if (IsOpenQuote(aliasMap, c))
|
|
{
|
|
curPart = ParserPart.QuotedParameter;
|
|
matchQuote = GetMatch(aliasMap, c);
|
|
continue;
|
|
}
|
|
curPart = ParserPart.Parameter;
|
|
}
|
|
}
|
|
|
|
//Has this parameter ended yet?
|
|
string argString = null;
|
|
if (curPart == ParserPart.Parameter)
|
|
{
|
|
if (curPos == endPos || char.IsWhiteSpace(c))
|
|
{
|
|
argString = argBuilder.ToString();
|
|
lastArgEndPos = curPos;
|
|
}
|
|
else
|
|
argBuilder.Append(c);
|
|
}
|
|
else if (curPart == ParserPart.QuotedParameter)
|
|
{
|
|
if (c == matchQuote)
|
|
{
|
|
argString = argBuilder.ToString(); //Remove quotes
|
|
lastArgEndPos = curPos + 1;
|
|
}
|
|
else
|
|
argBuilder.Append(c);
|
|
}
|
|
|
|
if (argString != null)
|
|
{
|
|
if (curParam == null)
|
|
{
|
|
if (command.IgnoreExtraArgs)
|
|
break;
|
|
else
|
|
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");
|
|
}
|
|
|
|
var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false);
|
|
if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches)
|
|
return ParseResult.FromError(typeReaderResult, curParam);
|
|
|
|
if (curParam.IsMultiple)
|
|
{
|
|
paramList.Add(typeReaderResult);
|
|
|
|
curPart = ParserPart.None;
|
|
}
|
|
else
|
|
{
|
|
argList.Add(typeReaderResult);
|
|
|
|
curParam = null;
|
|
curPart = ParserPart.None;
|
|
}
|
|
argBuilder.Clear();
|
|
}
|
|
}
|
|
|
|
if (curParam != null && curParam.IsRemainder)
|
|
{
|
|
var typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false);
|
|
if (!typeReaderResult.IsSuccess)
|
|
return ParseResult.FromError(typeReaderResult, curParam);
|
|
argList.Add(typeReaderResult);
|
|
}
|
|
|
|
if (isEscaping)
|
|
return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape.");
|
|
if (curPart == ParserPart.QuotedParameter)
|
|
return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete.");
|
|
|
|
//Add missing optionals
|
|
for (int i = argList.Count; i < command.Parameters.Count; i++)
|
|
{
|
|
var param = command.Parameters[i];
|
|
if (param.IsMultiple)
|
|
continue;
|
|
if (!param.IsOptional)
|
|
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters.");
|
|
argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue));
|
|
}
|
|
|
|
return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable());
|
|
}
|
|
}
|
|
}
|