diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 671c7acb..da445ae7 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using App.Metrics; @@ -61,9 +62,10 @@ namespace PluralKit.Bot /// public bool Match(ref string used, params string[] potentialMatches) { + var arg = PeekArgument(); foreach (var match in potentialMatches) { - if (PeekArgument().Equals(match, StringComparison.InvariantCultureIgnoreCase)) + if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase)) { used = PopArgument(); return true; @@ -81,6 +83,15 @@ namespace PluralKit.Bot string used = null; // Unused and unreturned, we just yeet it return Match(ref used, potentialMatches); } + + public bool MatchFlag(params string[] potentialMatches) + { + // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. + // Can assume the caller array only contains lowercase *and* the set below only contains lowercase + + var flags = _parameters.Flags(); + return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch)); + } public async Task Execute(Command commandDef, Func handler) { diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 328bb747..987d5b40 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -12,6 +12,29 @@ namespace PluralKit.Bot private readonly string _cmd; private int _ptr; + private ISet _flags = null; // Only parsed when requested first time + + private struct WordPosition + { + // Start of the word + internal int startPos; + + // End of the word + internal int endPos; + + // How much to advance word pointer afterwards to point at the start of the *next* word + internal int advanceAfterWord; + + internal bool wasQuoted; + + public WordPosition(int startPos, int endPos, int advanceAfterWord, bool wasQuoted) + { + this.startPos = startPos; + this.endPos = endPos; + this.advanceAfterWord = advanceAfterWord; + this.wasQuoted = wasQuoted; + } + } public Parameters(string cmd) { @@ -21,43 +44,90 @@ namespace PluralKit.Bot _ptr = 0; } + private void ParseFlags() + { + _flags = new HashSet(); + + var ptr = 0; + while (NextWordPosition(ptr) is { } wp) + { + ptr = wp.endPos + wp.advanceAfterWord; + + // Is this word a *flag* (as in, starts with a - AND is not quoted) + if (_cmd[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word) + + // Find the *end* of the flag start (technically allowing arbitrary amounts of dashes) + var flagNameStart = wp.startPos; + while (flagNameStart < _cmd.Length && _cmd[flagNameStart] == '-') + flagNameStart++; + + // Then add the word to the flag set + var word = _cmd.Substring(flagNameStart, wp.endPos - flagNameStart).Trim(); + if (word.Length > 0) + _flags.Add(word.ToLowerInvariant()); + } + } + public string Pop() { - var positions = NextWordPosition(); - if (positions == null) return ""; + // Loop to ignore and skip past flags + while (NextWordPosition(_ptr) is { } pos) + { + _ptr = pos.endPos + pos.advanceAfterWord; + if (_cmd[pos.startPos] == '-' && !pos.wasQuoted) continue; + return _cmd.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); + } - var (start, end, advance) = positions.Value; - _ptr = end + advance; - return _cmd.Substring(start, end - start).Trim(); + return ""; } public string Peek() { - var positions = NextWordPosition(); - if (positions == null) return ""; + // Loop to ignore and skip past flags, temp ptr so we don't move the real ptr + var ptr = _ptr; + while (NextWordPosition(ptr) is { } pos) + { + ptr = pos.endPos + pos.advanceAfterWord; + if (_cmd[pos.startPos] == '-' && !pos.wasQuoted) continue; + return _cmd.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); + } - var (start, end, _) = positions.Value; - return _cmd.Substring(start, end - start).Trim(); + return ""; } - public string Remainder() => _cmd.Substring(Math.Min(_ptr, _cmd.Length)).Trim(); + public ISet Flags() + { + if (_flags == null) ParseFlags(); + return _flags; + } + + public string Remainder() + { + // Skip all *leading* flags when taking the remainder + while (NextWordPosition(_ptr) is {} wp) + { + if (_cmd[wp.startPos] != '-' || wp.wasQuoted) break; + _ptr = wp.endPos + wp.advanceAfterWord; + } + + // *Then* get the remainder + return _cmd.Substring(Math.Min(_ptr, _cmd.Length)).Trim(); + } + public string FullCommand => _cmd; - // Returns tuple of (startpos, endpos, advanceafter) - // advanceafter is how much to move the pointer afterwards to point it - // at the start of the next word - private ValueTuple? NextWordPosition() + private WordPosition? NextWordPosition(int position) { // Is this the end of the string? - if (_cmd.Length <= _ptr) return null; + if (_cmd.Length <= position) return null; // Is this a quoted word? - if (_quotePairs.ContainsKey(_cmd[_ptr])) + if (_quotePairs.ContainsKey(_cmd[position])) { // This is a quoted word, find corresponding end quote and return span - var endQuote = _quotePairs[_cmd[_ptr]]; - var endQuotePosition = _cmd.IndexOf(endQuote, _ptr + 1); - + var endQuote = _quotePairs[_cmd[position]]; + var endQuotePosition = _cmd.IndexOf(endQuote, position + 1); + // Position after the end quote should be a space (or EOL) // Otherwise treat it as a standard word that's not quoted if (_cmd.Length == endQuotePosition + 1 || _cmd[endQuotePosition + 1] == ' ') @@ -66,16 +136,19 @@ namespace PluralKit.Bot { // This is an unterminated quoted word, just return the entire word including the start quote // TODO: should we do something else here? - return (_ptr, _cmd.Length, 0); + return new WordPosition(position, _cmd.Length, 0, false); } - return (_ptr + 1, endQuotePosition, 2); + return new WordPosition(position + 1, endQuotePosition, 2, true); } } - // Not a quoted word, just find the next space and return as appropriate - var wordEnd = _cmd.IndexOf(' ', _ptr + 1); - return wordEnd != -1 ? (_ptr, wordEnd, 1) : (_ptr, _cmd.Length, 0); + // Not a quoted word, just find the next space and return if it's the end of the command + var wordEnd = _cmd.IndexOf(' ', position + 1); + + return wordEnd == -1 + ? new WordPosition(position, _cmd.Length, 0, false) + : new WordPosition(position, wordEnd, 1, false); } } } \ No newline at end of file