package parser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/brettlangdon/forge/config"
|
|
"github.com/brettlangdon/forge/token"
|
|
)
|
|
|
|
type Parser struct {
|
|
settings *config.SectionValue
|
|
tokenizer *token.Tokenizer
|
|
cur_tok token.Token
|
|
cur_section *config.SectionValue
|
|
previous []*config.SectionValue
|
|
}
|
|
|
|
func (this *Parser) SyntaxError(msg string) error {
|
|
msg = fmt.Sprintf(
|
|
"Syntax error line <%d> column <%d>: %s",
|
|
this.cur_tok.Line,
|
|
this.cur_tok.Column,
|
|
msg,
|
|
)
|
|
return errors.New(msg)
|
|
}
|
|
|
|
func (this *Parser) readToken() token.Token {
|
|
this.cur_tok = this.tokenizer.NextToken()
|
|
return this.cur_tok
|
|
}
|
|
|
|
func (this *Parser) parseReference(starting_section *config.SectionValue, period bool) (config.ConfigValue, error) {
|
|
name := ""
|
|
if period == false {
|
|
name = this.cur_tok.Literal
|
|
}
|
|
for {
|
|
this.readToken()
|
|
if this.cur_tok.ID == token.PERIOD && period == false {
|
|
period = true
|
|
} else if period && this.cur_tok.ID == token.IDENTIFIER {
|
|
if len(name) > 0 {
|
|
name += "."
|
|
}
|
|
name += this.cur_tok.Literal
|
|
period = false
|
|
} else if this.cur_tok.ID == token.SEMICOLON {
|
|
break
|
|
} else {
|
|
msg := fmt.Sprintf("expected ';' instead found '%s'", this.cur_tok.Literal)
|
|
return nil, this.SyntaxError(msg)
|
|
}
|
|
}
|
|
if len(name) == 0 {
|
|
return nil, this.SyntaxError(
|
|
fmt.Sprintf("expected IDENTIFIER instead found %s", this.cur_tok.Literal),
|
|
)
|
|
}
|
|
|
|
if period {
|
|
return nil, this.SyntaxError(fmt.Sprintf("expected IDENTIFIER after PERIOD"))
|
|
}
|
|
|
|
value, err := starting_section.Resolve(name)
|
|
if err != nil {
|
|
err = errors.New("Reference error, " + err.Error())
|
|
}
|
|
return value, err
|
|
}
|
|
|
|
func (this *Parser) parseSetting(name string) error {
|
|
var value config.ConfigValue
|
|
this.readToken()
|
|
|
|
read_next := true
|
|
switch this.cur_tok.ID {
|
|
case token.STRING:
|
|
value = config.StringValue{
|
|
Name: name,
|
|
Value: this.cur_tok.Literal,
|
|
}
|
|
case token.BOOLEAN:
|
|
bool_val, err := strconv.ParseBool(this.cur_tok.Literal)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
value = config.BooleanValue{
|
|
Name: name,
|
|
Value: bool_val,
|
|
}
|
|
case token.NULL:
|
|
value = config.NullValue{
|
|
Name: name,
|
|
Value: nil,
|
|
}
|
|
case token.INTEGER:
|
|
int_val, err := strconv.ParseInt(this.cur_tok.Literal, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = config.IntegerValue{
|
|
Name: name,
|
|
Value: int_val,
|
|
}
|
|
case token.FLOAT:
|
|
float_val, err := strconv.ParseFloat(this.cur_tok.Literal, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = config.FloatValue{
|
|
Name: name,
|
|
Value: float_val,
|
|
}
|
|
case token.PERIOD:
|
|
reference, err := this.parseReference(this.cur_section, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = reference
|
|
read_next = false
|
|
case token.IDENTIFIER:
|
|
reference, err := this.parseReference(this.settings, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = reference
|
|
read_next = false
|
|
default:
|
|
return this.SyntaxError(
|
|
fmt.Sprintf("expected STRING, INTEGER, FLOAT, BOOLEAN or IDENTIFIER, instead found %s", this.cur_tok.ID),
|
|
)
|
|
}
|
|
|
|
if read_next {
|
|
this.readToken()
|
|
}
|
|
if this.cur_tok.ID != token.SEMICOLON {
|
|
msg := fmt.Sprintf("expected ';' instead found '%s'", this.cur_tok.Literal)
|
|
return this.SyntaxError(msg)
|
|
}
|
|
this.readToken()
|
|
|
|
this.cur_section.Set(name, value)
|
|
return nil
|
|
}
|
|
|
|
func (this *Parser) parseInclude() error {
|
|
if this.cur_tok.ID != token.STRING {
|
|
msg := fmt.Sprintf("expected STRING instead found '%s'", this.cur_tok.ID)
|
|
return this.SyntaxError(msg)
|
|
}
|
|
pattern := this.cur_tok.Literal
|
|
|
|
this.readToken()
|
|
if this.cur_tok.ID != token.SEMICOLON {
|
|
msg := fmt.Sprintf("expected ';' instead found '%s'", this.cur_tok.Literal)
|
|
return this.SyntaxError(msg)
|
|
}
|
|
|
|
filenames, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
old_tokenizer := this.tokenizer
|
|
for _, filename := range filenames {
|
|
reader, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
this.cur_section.AddInclude(filename)
|
|
this.tokenizer = token.NewTokenizer(reader)
|
|
this.Parse()
|
|
}
|
|
this.tokenizer = old_tokenizer
|
|
this.readToken()
|
|
return nil
|
|
}
|
|
|
|
func (this *Parser) parseSection(name string) error {
|
|
section := config.NewNamedSection(name)
|
|
this.cur_section.Set(name, section)
|
|
this.previous = append(this.previous, this.cur_section)
|
|
this.cur_section = section
|
|
return nil
|
|
}
|
|
|
|
func (this *Parser) endSection() error {
|
|
if len(this.previous) == 0 {
|
|
return this.SyntaxError("unexpected section end '}'")
|
|
}
|
|
|
|
p_len := len(this.previous)
|
|
previous := this.previous[p_len-1]
|
|
this.previous = this.previous[0 : p_len-1]
|
|
this.cur_section = previous
|
|
return nil
|
|
}
|
|
|
|
func (this *Parser) Parse() error {
|
|
this.readToken()
|
|
for {
|
|
if this.cur_tok.ID == token.EOF {
|
|
break
|
|
}
|
|
tok := this.cur_tok
|
|
this.readToken()
|
|
switch tok.ID {
|
|
case token.COMMENT:
|
|
this.cur_section.AddComment(tok.Literal)
|
|
case token.INCLUDE:
|
|
this.parseInclude()
|
|
case token.IDENTIFIER:
|
|
if this.cur_tok.ID == token.LBRACKET {
|
|
err := this.parseSection(tok.Literal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
this.readToken()
|
|
} else if this.cur_tok.ID == token.EQUAL {
|
|
err := this.parseSetting(tok.Literal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case token.RBRACKET:
|
|
err := this.endSection()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return this.SyntaxError(fmt.Sprintf("unexpected token %s", tok))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ParseFile(filename string) (settings *config.SectionValue, err error) {
|
|
reader, err := os.Open(filename)
|
|
if err != nil {
|
|
return settings, err
|
|
}
|
|
return ParseReader(reader)
|
|
}
|
|
|
|
func ParseReader(reader io.Reader) (*config.SectionValue, error) {
|
|
settings := config.NewAnonymousSection()
|
|
parser := &Parser{
|
|
tokenizer: token.NewTokenizer(reader),
|
|
settings: settings,
|
|
cur_section: settings,
|
|
previous: make([]*config.SectionValue, 0),
|
|
}
|
|
err := parser.Parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(parser.previous) > 0 {
|
|
return nil, parser.SyntaxError("expected end of section, instead found EOF")
|
|
}
|
|
|
|
return settings, nil
|
|
}
|