arikawa/bot/extras/shellwords/shellwords.go

150 lines
2.3 KiB
Go
Raw Normal View History

package shellwords
import (
"fmt"
"strings"
)
// WordOffset is the offset from the position cursor to print on the error.
const WordOffset = 7
var escaper = strings.NewReplacer(
"`", "\\`",
"@", "\\@",
"\\", "\\\\",
)
2020-01-26 07:17:18 +00:00
type ErrParse struct {
Position int
Words string // joined
}
func (e ErrParse) Error() string {
// Magic number 5.
var a = max(0, e.Position-WordOffset)
var b = min(len(e.Words), e.Position+WordOffset)
var word = e.Words[a:b]
var uidx = e.Position - a
errstr := strings.Builder{}
errstr.WriteString("Unexpected quote or escape")
// Do a bound check.
if uidx+1 > len(word) {
// Invalid.
errstr.WriteString(".")
return errstr.String()
}
// Write the pre-underline part.
fmt.Fprintf(
&errstr, ": %s__%s__",
escaper.Replace(word[:uidx]),
escaper.Replace(string(word[uidx:])),
)
return errstr.String()
}
// Parse parses the given text to a slice of words.
2020-01-26 07:17:18 +00:00
func Parse(line string) ([]string, error) {
var args []string
2020-01-26 07:17:18 +00:00
var escaped, doubleQuoted, singleQuoted bool
var buf strings.Builder
buf.Grow(len(line))
got := false
2020-01-26 07:17:18 +00:00
cursor := 0
2020-01-26 07:17:18 +00:00
runes := []rune(line)
for _, r := range runes {
if escaped {
buf.WriteRune(r)
escaped = false
continue
}
if r == '\\' {
if singleQuoted {
buf.WriteRune(r)
} else {
escaped = true
}
continue
}
if isSpace(r) {
switch {
2020-01-26 07:17:18 +00:00
case singleQuoted, doubleQuoted:
buf.WriteRune(r)
case got:
cursor += buf.Len()
args = append(args, buf.String())
buf.Reset()
got = false
}
continue
}
switch r {
case '"':
if !singleQuoted {
if doubleQuoted {
got = true
}
doubleQuoted = !doubleQuoted
continue
}
case '\'', '`':
if !doubleQuoted {
if singleQuoted {
got = true
}
singleQuoted = !singleQuoted
continue
}
}
got = true
buf.WriteRune(r)
}
if got {
args = append(args, buf.String())
}
2020-01-26 07:17:18 +00:00
if escaped || singleQuoted || doubleQuoted {
return args, &ErrParse{
Position: cursor + buf.Len(),
Words: strings.Join(args, " "),
2020-01-26 07:17:18 +00:00
}
}
return args, nil
}
2020-01-26 07:17:18 +00:00
func isSpace(r rune) bool {
switch r {
case ' ', '\t', '\r', '\n', ' ':
2020-01-26 07:17:18 +00:00
return true
}
return false
}
func min(i, j int) int {
if i < j {
return i
}
return j
}
func max(i, j int) int {
if i < j {
return j
}
return i
}