Bot: Allow hanging quotes if command has a custom parser

This commit is contained in:
diamondburned 2020-08-11 13:44:10 -07:00
parent 94cca0adca
commit 3db68bcb0e
4 changed files with 117 additions and 31 deletions

View File

@ -72,6 +72,8 @@ func (r ArgumentParts) Usage() string {
// CustomParser has a CustomParse method, which would be passed in the full
// message content with the prefix and command trimmed. This is used
// for commands that require more advanced parsing than the default parser.
//
// Keep in mind that this does not trim arguments before it.
type CustomParser interface {
CustomParse(arguments string) error
}

View File

@ -125,13 +125,12 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
}
// parse arguments
parts, err := ctx.ParseArgs(content)
if err != nil {
return errors.Wrap(err, "failed to parse command")
}
parts, parseErr := ctx.ParseArgs(content)
// We're not checking parse errors yet, as raw arguments may be able to
// ignore it.
if len(parts) == 0 {
return nil // ???
return parseErr
}
// Find the command and subcommand.
@ -258,6 +257,10 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
// If the argument wants all arguments in string:
case last.custom != nil:
// Ignore parser errors. This allows custom commands sliced away to
// have erroneous hanging quotes.
parseErr = nil
// Manual string seeking is a must here. This is because the string
// could contain multiple whitespaces, and the parser would not
// count them.
@ -268,9 +271,17 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
}
// Seek to the string.
if i := strings.Index(content, seekTo); i > -1 {
var i = strings.Index(content, seekTo)
// Edge case if the subcommand is the same as the command.
if cmd.Command == sub.Command {
// Seek again past the command.
i = strings.Index(content[i+len(seekTo):], seekTo)
}
if i > -1 {
// Seek past the substring.
i += len(seekTo)
content = strings.TrimSpace(content[i:])
}
@ -292,6 +303,11 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
argv = append(argv, v)
}
// Check for parsing errors after parsing arguments.
if parseErr != nil {
return parseErr
}
Call:
// call the function and parse the error return value
v, err := cmd.call(value, argv...)

View File

@ -1,19 +1,31 @@
package shellwords
import (
"errors"
"fmt"
"strings"
)
type ErrParse struct {
Line string
Position string
Position int
ErrorStart,
ErrorPart,
ErrorEnd string
}
func (e ErrParse) Error() string {
return fmt.Sprintf(
"Unexpected quote or escape: %s__%s__%s",
e.ErrorStart, e.ErrorPart, e.ErrorEnd,
)
}
// Parse parses the given text to a slice of words.
func Parse(line string) ([]string, error) {
var args []string
buf := ""
var escaped, doubleQuoted, singleQuoted bool
backtick := ""
var buf strings.Builder
buf.Grow(len(line))
got := false
cursor := 0
@ -22,14 +34,14 @@ func Parse(line string) ([]string, error) {
for _, r := range runes {
if escaped {
buf += string(r)
buf.WriteRune(r)
escaped = false
continue
}
if r == '\\' {
if singleQuoted {
buf += string(r)
buf.WriteRune(r)
} else {
escaped = true
}
@ -39,12 +51,11 @@ func Parse(line string) ([]string, error) {
if isSpace(r) {
switch {
case singleQuoted, doubleQuoted:
buf += string(r)
backtick += string(r)
buf.WriteRune(r)
case got:
cursor += len(buf)
args = append(args, buf)
buf = ""
cursor += buf.Len()
args = append(args, buf.String())
buf.Reset()
got = false
}
continue
@ -59,22 +70,28 @@ func Parse(line string) ([]string, error) {
doubleQuoted = !doubleQuoted
continue
}
case '\'':
case '\'', '`':
if !doubleQuoted {
if singleQuoted {
got = true
}
// // If this is a backtick, then write it.
// if r == '`' {
// buf.WriteByte('`')
// }
singleQuoted = !singleQuoted
continue
}
}
got = true
buf += string(r)
buf.WriteRune(r)
}
if got {
args = append(args, buf)
args = append(args, buf.String())
}
if escaped || singleQuoted || doubleQuoted {
@ -83,18 +100,15 @@ func Parse(line string) ([]string, error) {
pos = cursor + 5
start = string(runes[max(cursor-100, 0) : pos-1])
end = string(runes[pos+1 : min(cursor+100, len(runes))])
part = ""
part = string(runes[max(pos-1, 0):min(len(runes), pos+2)])
)
for i := pos - 1; i >= 0 && i < len(runes) && i < pos+2; i++ {
if runes[i] == '\\' {
part += "\\"
}
part += string(runes[i])
return args, &ErrParse{
Position: cursor,
ErrorStart: start,
ErrorPart: part,
ErrorEnd: end,
}
return nil, errors.New(
"Unexpected quote or escape: " + start + "__" + part + "__" + end)
}
return args, nil
@ -102,7 +116,7 @@ func Parse(line string) ([]string, error) {
func isSpace(r rune) bool {
switch r {
case ' ', '\t', '\r', '\n':
case ' ', '\t', '\r', '\n', ' ':
return true
}
return false

View File

@ -0,0 +1,54 @@
package shellwords
import (
"reflect"
"testing"
)
type wordsTest struct {
line string
args []string
doErr bool
}
func TestParse(t *testing.T) {
var tests = []wordsTest{
{
`this is a "test"`,
[]string{"this", "is", "a", "test"},
false,
},
{
`hanging "quote`,
[]string{"hanging", "quote"},
true,
},
{
`Hello, 世界`,
[]string{"Hello,", "世界"},
false,
},
{
"this is `inline code`",
[]string{"this", "is", "inline code"},
false,
},
{
"how about a ```go\npackage main\n```\ngo code?",
[]string{"how", "about", "a", "go\npackage main\n", "go", "code?"},
false,
},
}
for _, test := range tests {
w, err := Parse(test.line)
if err != nil && !test.doErr {
t.Errorf("Error at %q: %v", test.line, err)
continue
}
if !reflect.DeepEqual(w, test.args) {
t.Errorf("Inequality:\n%#v !=\n%#v", w, test.args)
}
}
}