Bot: Allow hanging quotes if command has a custom parser
This commit is contained in:
parent
94cca0adca
commit
3db68bcb0e
|
@ -72,6 +72,8 @@ func (r ArgumentParts) Usage() string {
|
||||||
// CustomParser has a CustomParse method, which would be passed in the full
|
// CustomParser has a CustomParse method, which would be passed in the full
|
||||||
// message content with the prefix and command trimmed. This is used
|
// message content with the prefix and command trimmed. This is used
|
||||||
// for commands that require more advanced parsing than the default parser.
|
// 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 {
|
type CustomParser interface {
|
||||||
CustomParse(arguments string) error
|
CustomParse(arguments string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,13 +125,12 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse arguments
|
// parse arguments
|
||||||
parts, err := ctx.ParseArgs(content)
|
parts, parseErr := ctx.ParseArgs(content)
|
||||||
if err != nil {
|
// We're not checking parse errors yet, as raw arguments may be able to
|
||||||
return errors.Wrap(err, "failed to parse command")
|
// ignore it.
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return nil // ???
|
return parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the command and subcommand.
|
// 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:
|
// If the argument wants all arguments in string:
|
||||||
case last.custom != nil:
|
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
|
// Manual string seeking is a must here. This is because the string
|
||||||
// could contain multiple whitespaces, and the parser would not
|
// could contain multiple whitespaces, and the parser would not
|
||||||
// count them.
|
// count them.
|
||||||
|
@ -268,9 +271,17 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to the string.
|
// 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.
|
// Seek past the substring.
|
||||||
i += len(seekTo)
|
i += len(seekTo)
|
||||||
|
|
||||||
content = strings.TrimSpace(content[i:])
|
content = strings.TrimSpace(content[i:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,6 +303,11 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
||||||
argv = append(argv, v)
|
argv = append(argv, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for parsing errors after parsing arguments.
|
||||||
|
if parseErr != nil {
|
||||||
|
return parseErr
|
||||||
|
}
|
||||||
|
|
||||||
Call:
|
Call:
|
||||||
// call the function and parse the error return value
|
// call the function and parse the error return value
|
||||||
v, err := cmd.call(value, argv...)
|
v, err := cmd.call(value, argv...)
|
||||||
|
|
|
@ -1,19 +1,31 @@
|
||||||
package shellwords
|
package shellwords
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrParse struct {
|
type ErrParse struct {
|
||||||
Line string
|
Position int
|
||||||
Position string
|
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) {
|
func Parse(line string) ([]string, error) {
|
||||||
var args []string
|
var args []string
|
||||||
buf := ""
|
|
||||||
var escaped, doubleQuoted, singleQuoted bool
|
var escaped, doubleQuoted, singleQuoted bool
|
||||||
backtick := ""
|
|
||||||
|
var buf strings.Builder
|
||||||
|
buf.Grow(len(line))
|
||||||
|
|
||||||
got := false
|
got := false
|
||||||
cursor := 0
|
cursor := 0
|
||||||
|
@ -22,14 +34,14 @@ func Parse(line string) ([]string, error) {
|
||||||
|
|
||||||
for _, r := range runes {
|
for _, r := range runes {
|
||||||
if escaped {
|
if escaped {
|
||||||
buf += string(r)
|
buf.WriteRune(r)
|
||||||
escaped = false
|
escaped = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if r == '\\' {
|
if r == '\\' {
|
||||||
if singleQuoted {
|
if singleQuoted {
|
||||||
buf += string(r)
|
buf.WriteRune(r)
|
||||||
} else {
|
} else {
|
||||||
escaped = true
|
escaped = true
|
||||||
}
|
}
|
||||||
|
@ -39,12 +51,11 @@ func Parse(line string) ([]string, error) {
|
||||||
if isSpace(r) {
|
if isSpace(r) {
|
||||||
switch {
|
switch {
|
||||||
case singleQuoted, doubleQuoted:
|
case singleQuoted, doubleQuoted:
|
||||||
buf += string(r)
|
buf.WriteRune(r)
|
||||||
backtick += string(r)
|
|
||||||
case got:
|
case got:
|
||||||
cursor += len(buf)
|
cursor += buf.Len()
|
||||||
args = append(args, buf)
|
args = append(args, buf.String())
|
||||||
buf = ""
|
buf.Reset()
|
||||||
got = false
|
got = false
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
@ -59,22 +70,28 @@ func Parse(line string) ([]string, error) {
|
||||||
doubleQuoted = !doubleQuoted
|
doubleQuoted = !doubleQuoted
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case '\'':
|
case '\'', '`':
|
||||||
if !doubleQuoted {
|
if !doubleQuoted {
|
||||||
if singleQuoted {
|
if singleQuoted {
|
||||||
got = true
|
got = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // If this is a backtick, then write it.
|
||||||
|
// if r == '`' {
|
||||||
|
// buf.WriteByte('`')
|
||||||
|
// }
|
||||||
|
|
||||||
singleQuoted = !singleQuoted
|
singleQuoted = !singleQuoted
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
got = true
|
got = true
|
||||||
buf += string(r)
|
buf.WriteRune(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got {
|
if got {
|
||||||
args = append(args, buf)
|
args = append(args, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if escaped || singleQuoted || doubleQuoted {
|
if escaped || singleQuoted || doubleQuoted {
|
||||||
|
@ -83,18 +100,15 @@ func Parse(line string) ([]string, error) {
|
||||||
pos = cursor + 5
|
pos = cursor + 5
|
||||||
start = string(runes[max(cursor-100, 0) : pos-1])
|
start = string(runes[max(cursor-100, 0) : pos-1])
|
||||||
end = string(runes[pos+1 : min(cursor+100, len(runes))])
|
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++ {
|
return args, &ErrParse{
|
||||||
if runes[i] == '\\' {
|
Position: cursor,
|
||||||
part += "\\"
|
ErrorStart: start,
|
||||||
}
|
ErrorPart: part,
|
||||||
part += string(runes[i])
|
ErrorEnd: end,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New(
|
|
||||||
"Unexpected quote or escape: " + start + "__" + part + "__" + end)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return args, nil
|
return args, nil
|
||||||
|
@ -102,7 +116,7 @@ func Parse(line string) ([]string, error) {
|
||||||
|
|
||||||
func isSpace(r rune) bool {
|
func isSpace(r rune) bool {
|
||||||
switch r {
|
switch r {
|
||||||
case ' ', '\t', '\r', '\n':
|
case ' ', '\t', '\r', '\n', ' ':
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue