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

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());
}
}
}