gob/shell/scanner/scanner.go

198 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package scanner
import (
"bufio"
"errors"
"io"
"unicode/utf8"
"gitea.zaclys.com/bvaudour/gob/collection"
)
// Tokenizer est une interface implémentant une fonction de splittage.
type Tokenizer interface {
Split(data []byte, atEOF bool) (advance int, token []byte, err error)
}
type tk struct {
quote bool
escape bool
spaces []rune
raw bool
}
func runeToBytes(r rune) []byte {
l := utf8.RuneLen(r)
b := make([]byte, l)
utf8.EncodeRune(b, r)
return b
}
func (t tk) spl(q, e, s collection.Set[rune], start int, data []byte) (advance int, token []byte, esc, quote rune, done bool) {
// Scan until space, marking end of word.
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) {
return i + width, token, esc, quote, true
}
if e.Contains(esc) {
if q.Contains(quote) && !e.Contains(r) && r != quote {
token = append(token, runeToBytes(esc)...)
}
token = append(token, runeToBytes(r)...)
esc = 0
} else if e.Contains(r) {
esc = r
} else if q.Contains(r) {
if !q.Contains(quote) {
quote = r
} else if quote == r {
quote = 0
} else {
token = append(token, runeToBytes(r)...)
}
} else {
token = append(token, runeToBytes(r)...)
}
}
return
}
func (t tk) rawSpl(q, e, s collection.Set[rune], start int, data []byte) (advance int, token []byte, esc, quote rune, done bool) {
// Scan until space, marking end of word.
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) {
return i + width, token, esc, quote, true
}
token = append(token, runeToBytes(r)...)
if e.Contains(esc) {
esc = 0
} else if e.Contains(r) {
esc = r
} else if q.Contains(quote) {
if quote == r {
quote = 0
}
} else if q.Contains(r) {
quote = r
}
}
return
}
func (t tk) Split(data []byte, atEOF bool) (advance int, token []byte, err error) {
q, e := collection.NewSet[rune](), collection.NewSet[rune]()
s := collection.NewSet(t.spaces...)
if t.quote {
q.Add('\'', '"')
}
if t.escape {
e.Add('\\')
}
// Skip leading spaces.
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !s.Contains(r) {
break
}
}
// Scan until space, marking end of word.
var ok bool
var esc, quote rune
if t.raw {
advance, token, esc, quote, ok = t.rawSpl(q, e, s, start, data)
} else {
advance, token, esc, quote, ok = t.spl(q, e, s, start, data)
}
if ok {
return
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
if e.Contains(esc) || q.Contains(quote) {
return start, nil, errors.New("Incomplete token")
}
return len(data), token, nil
}
// Request more data.
return start, nil, nil
}
// NewTokenizer retourne un tokenizer darguments de ligne de commande.
//
// Le split seffectue au niveau des caractères spaces fourni (tous les espaces
// si aucun de fourni) sauf si ces caractères sont échappés
// (si escape) ou entre guillemets (si quote)
// Par exemple, prenons la chaîne suivante :
// unmot une\ phrase "une deuxième\" phrase"
//
// Le résultat va être décomposé en 3 éléments :
// - unmot
// - une phrase
// - une deuxième phrase
func NewTokenizer(quote, escape bool, spaces ...rune) Tokenizer {
if len(spaces) == 0 {
spaces = []rune(" \t\n\v\f\r")
}
return tk{
quote: quote,
escape: escape,
spaces: spaces,
}
}
// NewRawTokenizer retourne un tokenizer darguments de ligne de commande.
//
// Le split seffectue au niveau des caractères spaces fourni (tous les espaces
// si aucun de fourni) sauf si ces caractères sont échappés
// (si escape) ou entre guillemets (si quote)
// Par exemple, prenons la chaîne suivante :
// unmot une\ phrase "une deuxième\" phrase"
//
// Le résultat va être décomposé en 3 éléments :
// - unmot
// - une\ phrase
// - "une deuxième\" phrase"
func NewRawTokenizer(quote, escape bool, spaces ...rune) Tokenizer {
if len(spaces) == 0 {
spaces = []rune(" \t\n\v\f\r")
}
return tk{
quote: quote,
escape: escape,
spaces: spaces,
raw: true,
}
}
// NewScanner retourne un nouveau scanner utilisant le tokenizer spécifié
// pour la fonction de splitage (NewTokenizer(true, true) si aucun tokenizer fourni).
func NewScanner(r io.Reader, t ...Tokenizer) *bufio.Scanner {
var s bufio.SplitFunc
if len(t) > 0 {
s = t[0].Split
} else {
s = (NewTokenizer(true, true)).Split
}
sc := bufio.NewScanner(r)
sc.Split(s)
return sc
}
// NewRawScanner retourne un scanner utilisant RawTokenizer avec détection des quotes et de léchappement.
func NewRawScanner(r io.Reader) *bufio.Scanner {
return NewScanner(r, NewRawTokenizer(true, true))
}