alrm/config/tokenizer.go

158 lines
2.8 KiB
Go

package config
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"unicode"
)
const (
TK_NONE = iota
TK_VAL
TK_QUOTE
TK_COMMENT
)
type Tokenizer struct {
curline int
repline int
file *os.File
reader *bufio.Reader
text string
err error
}
func NewTokenizer(fn string) (*Tokenizer, error) {
var err error
tk := &Tokenizer{curline: 1}
tk.file, err = os.Open(fn)
if err != nil {
return nil, err
}
tk.reader = bufio.NewReader(tk.file)
return tk, nil
}
func (t *Tokenizer) Close() error {
return t.file.Close()
}
func (t *Tokenizer) Scan() bool {
t.repline = t.curline
state := TK_NONE
t.text = ""
var b strings.Builder
var quo rune
for {
var r rune
r, _, t.err = t.reader.ReadRune()
if t.err != nil {
break
}
if r == unicode.ReplacementChar {
t.err = fmt.Errorf("invalid utf-8 encoding on line %s", t.repline)
break
}
switch state {
case TK_NONE:
// When between values, increment both the reported line
// and the current line, since there's not yet anything
// to report
if r == '\n' {
t.repline++
t.curline++
}
// If we're between values and we encounter a space
// or a control character, ignore it
if unicode.IsSpace(r) || unicode.IsControl(r) {
continue
}
// If we're between values and we encounter a #, it's
// the beginning of a comment
if r == '#' {
state = TK_COMMENT
continue
}
// If we're between values and we get a quote character
// treat it as the beginning of a string literal
if r == '"' || r == '\'' || r == '`' {
state = TK_QUOTE
quo = r
continue
}
b.WriteRune(r)
state = TK_VAL
case TK_VAL:
// In values, only increment the current line, so
// if an error is reported, it reports the line
// the value starts on
if r == '\n' {
t.curline++
}
// If we're in a normal value and we encounter a space
// or a control character, end value
if unicode.IsSpace(r) || unicode.IsControl(r) {
goto end
}
b.WriteRune(r)
case TK_QUOTE:
// In quotes, only increment the current line, so
// if an error is reported, it reports the line
// the quoted value starts on
if r == '\n' {
t.curline++
}
// End this quote if it's another quote of the same rune
if r == quo {
goto end
}
b.WriteRune(r)
case TK_COMMENT:
// Comments are ignored, until a new line is encounter
// at which point, increment the current and reported line
if r == '\n' {
t.curline++
t.repline++
state = TK_NONE
}
continue
}
}
end:
if t.err == nil || t.err == io.EOF {
t.text = b.String()
}
return t.err == nil
}
func (t *Tokenizer) Text() string {
return t.text
}
func (t *Tokenizer) Line() int {
return t.repline
}
func (t *Tokenizer) Err() error {
if t.err == io.EOF {
return nil
}
return t.err
}