* Add ability to support different types of quotation marks * Added normal quotation mark to list of aliases, removed single quote mark * clean up leftover changes from testing * change quotation mark parsing to use a map of matching pairs * remove commented out code * Fix conventions of the command parser utility functions * change storage type of alias dictionary to be IReadOnlyDictionary * revert type of CommandServiceConfig QuotationMarkAliasMap to Dictionary * minor formatting changes to CommandParser * remove unnecessary whitespace * Move aliases outside of CommandInfo class * copy IReadOnlyDictionary to ImmutableDictionary * minor syntax changes in CommandServiceConfig * add newline before namespace for consistency * newline formatting tweak * simplification of GetMatch method for CommandParser * add more quote unicode punctuation pairs * add check for null value when building ImmutableDictionary * Move default alias map into a separate source file * Ensure that the collection passed into command service is not nulltags/2.0
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -13,8 +14,7 @@ namespace Discord.Commands | |||||
| Parameter, | Parameter, | ||||
| QuotedParameter | QuotedParameter | ||||
| } | } | ||||
| public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) | |||||
| 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; | ParameterInfo curParam = null; | ||||
| StringBuilder argBuilder = new StringBuilder(input.Length); | StringBuilder argBuilder = new StringBuilder(input.Length); | ||||
| @@ -24,7 +24,27 @@ namespace Discord.Commands | |||||
| var argList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | var argList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | ||||
| var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | ||||
| bool isEscaping = false; | bool isEscaping = false; | ||||
| char c; | |||||
| 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++) | for (int curPos = startPos; curPos <= endPos; curPos++) | ||||
| { | { | ||||
| @@ -74,9 +94,11 @@ namespace Discord.Commands | |||||
| argBuilder.Append(c); | argBuilder.Append(c); | ||||
| continue; | continue; | ||||
| } | } | ||||
| if (c == '\"') | |||||
| if (IsOpenQuote(aliasMap, c)) | |||||
| { | { | ||||
| curPart = ParserPart.QuotedParameter; | curPart = ParserPart.QuotedParameter; | ||||
| matchQuote = GetMatch(aliasMap, c); | |||||
| continue; | continue; | ||||
| } | } | ||||
| curPart = ParserPart.Parameter; | curPart = ParserPart.Parameter; | ||||
| @@ -97,7 +119,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| else if (curPart == ParserPart.QuotedParameter) | else if (curPart == ParserPart.QuotedParameter) | ||||
| { | { | ||||
| if (c == '\"') | |||||
| if (c == matchQuote) | |||||
| { | { | ||||
| argString = argBuilder.ToString(); //Remove quotes | argString = argBuilder.ToString(); //Remove quotes | ||||
| lastArgEndPos = curPos + 1; | lastArgEndPos = curPos + 1; | ||||
| @@ -1,4 +1,4 @@ | |||||
| using System; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -32,6 +32,7 @@ namespace Discord.Commands | |||||
| internal readonly RunMode _defaultRunMode; | internal readonly RunMode _defaultRunMode; | ||||
| internal readonly Logger _cmdLogger; | internal readonly Logger _cmdLogger; | ||||
| internal readonly LogManager _logManager; | internal readonly LogManager _logManager; | ||||
| internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; | |||||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | ||||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | ||||
| @@ -45,6 +46,7 @@ namespace Discord.Commands | |||||
| _ignoreExtraArgs = config.IgnoreExtraArgs; | _ignoreExtraArgs = config.IgnoreExtraArgs; | ||||
| _separatorChar = config.SeparatorChar; | _separatorChar = config.SeparatorChar; | ||||
| _defaultRunMode = config.DefaultRunMode; | _defaultRunMode = config.DefaultRunMode; | ||||
| _quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary(); | |||||
| if (_defaultRunMode == RunMode.Default) | if (_defaultRunMode == RunMode.Default) | ||||
| throw new InvalidOperationException("The default run mode cannot be set to Default."); | throw new InvalidOperationException("The default run mode cannot be set to Default."); | ||||
| @@ -337,7 +339,6 @@ namespace Discord.Commands | |||||
| public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | ||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
| var searchResult = Search(context, input); | var searchResult = Search(context, input); | ||||
| if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
| return searchResult; | return searchResult; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -18,6 +19,10 @@ namespace Discord.Commands | |||||
| /// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary> | /// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary> | ||||
| public bool ThrowOnError { get; set; } = true; | public bool ThrowOnError { get; set; } = true; | ||||
| /// <summary> Collection of aliases that can wrap strings for command parsing. | |||||
| /// represents the opening quotation mark and the value is the corresponding closing mark.</summary> | |||||
| public Dictionary<char, char> QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; | |||||
| /// <summary> Determines whether extra parameters should be ignored. </summary> | /// <summary> Determines whether extra parameters should be ignored. </summary> | ||||
| public bool IgnoreExtraArgs { get; set; } = false; | public bool IgnoreExtraArgs { get; set; } = false; | ||||
| } | } | ||||
| @@ -1,4 +1,4 @@ | |||||
| using Discord.Commands.Builders; | |||||
| using Discord.Commands.Builders; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -121,7 +121,8 @@ namespace Discord.Commands | |||||
| return ParseResult.FromError(preconditionResult); | return ParseResult.FromError(preconditionResult); | ||||
| string input = searchResult.Text.Substring(startIndex); | string input = searchResult.Text.Substring(startIndex); | ||||
| return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); | |||||
| return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false); | |||||
| } | } | ||||
| public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | ||||
| @@ -0,0 +1,95 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text; | |||||
| using System.Globalization; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| /// <summary> | |||||
| /// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig | |||||
| /// </summary> | |||||
| internal static class QuotationAliasUtils | |||||
| { | |||||
| /// <summary> | |||||
| /// Generates an IEnumerable of characters representing open-close pairs of | |||||
| /// quotation punctuation. | |||||
| /// </summary> | |||||
| internal static Dictionary<char, char> GetDefaultAliasMap | |||||
| { | |||||
| get | |||||
| { | |||||
| // Output of a gist provided by https://gist.github.com/ufcpp | |||||
| // https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5 | |||||
| // This was not used for the implementation because of incompatibility with netstandard1.1 | |||||
| return new Dictionary<char, char> { | |||||
| {'\"', '\"' }, | |||||
| {'«', '»' }, | |||||
| {'‘', '’' }, | |||||
| {'“', '”' }, | |||||
| {'„', '‟' }, | |||||
| {'‹', '›' }, | |||||
| {'‚', '‛' }, | |||||
| {'《', '》' }, | |||||
| {'〈', '〉' }, | |||||
| {'「', '」' }, | |||||
| {'『', '』' }, | |||||
| {'〝', '〞' }, | |||||
| {'﹁', '﹂' }, | |||||
| {'﹃', '﹄' }, | |||||
| {'"', '"' }, | |||||
| {''', ''' }, | |||||
| {'「', '」' }, | |||||
| {'(', ')' }, | |||||
| {'༺', '༻' }, | |||||
| {'༼', '༽' }, | |||||
| {'᚛', '᚜' }, | |||||
| {'⁅', '⁆' }, | |||||
| {'⌈', '⌉' }, | |||||
| {'⌊', '⌋' }, | |||||
| {'❨', '❩' }, | |||||
| {'❪', '❫' }, | |||||
| {'❬', '❭' }, | |||||
| {'❮', '❯' }, | |||||
| {'❰', '❱' }, | |||||
| {'❲', '❳' }, | |||||
| {'❴', '❵' }, | |||||
| {'⟅', '⟆' }, | |||||
| {'⟦', '⟧' }, | |||||
| {'⟨', '⟩' }, | |||||
| {'⟪', '⟫' }, | |||||
| {'⟬', '⟭' }, | |||||
| {'⟮', '⟯' }, | |||||
| {'⦃', '⦄' }, | |||||
| {'⦅', '⦆' }, | |||||
| {'⦇', '⦈' }, | |||||
| {'⦉', '⦊' }, | |||||
| {'⦋', '⦌' }, | |||||
| {'⦍', '⦎' }, | |||||
| {'⦏', '⦐' }, | |||||
| {'⦑', '⦒' }, | |||||
| {'⦓', '⦔' }, | |||||
| {'⦕', '⦖' }, | |||||
| {'⦗', '⦘' }, | |||||
| {'⧘', '⧙' }, | |||||
| {'⧚', '⧛' }, | |||||
| {'⧼', '⧽' }, | |||||
| {'⸂', '⸃' }, | |||||
| {'⸄', '⸅' }, | |||||
| {'⸉', '⸊' }, | |||||
| {'⸌', '⸍' }, | |||||
| {'⸜', '⸝' }, | |||||
| {'⸠', '⸡' }, | |||||
| {'⸢', '⸣' }, | |||||
| {'⸤', '⸥' }, | |||||
| {'⸦', '⸧' }, | |||||
| {'⸨', '⸩' }, | |||||
| {'【', '】'}, | |||||
| {'〔', '〕' }, | |||||
| {'〖', '〗' }, | |||||
| {'〘', '〙' }, | |||||
| {'〚', '〛' } | |||||
| }; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||