package forge import ( "errors" "fmt" "io" "os" "path/filepath" "strconv" "github.com/brettlangdon/forge/token" ) // Parser is a struct to hold data necessary for parsing a config from a scanner type Parser struct { files []string settings *Section scanner *Scanner curTok token.Token curSection *Section previous []*Section } // NewParser will create and initialize a new Parser from a provided io.Reader func NewParser(reader io.Reader) *Parser { settings := NewSection() return &Parser{ files: make([]string, 0), scanner: NewScanner(reader), settings: settings, curSection: settings, previous: make([]*Section, 0), } } // NewFileParser will create and initialize a new Parser from a provided from a filename string func NewFileParser(filename string) (*Parser, error) { reader, err := os.Open(filename) defer reader.Close() if err != nil { return nil, err } parser := NewParser(reader) parser.addFile(filename) return parser, nil } func (parser *Parser) addFile(filename string) { parser.files = append(parser.files, filename) } func (parser *Parser) hasParsed(search string) bool { for _, filename := range parser.files { if filename == search { return true } } return false } func (parser *Parser) syntaxError(msg string) error { msg = fmt.Sprintf( "syntax error line <%d> column <%d>: %s", parser.curTok.Line, parser.curTok.Column, msg, ) return errors.New(msg) } func (parser *Parser) readToken() token.Token { parser.curTok = parser.scanner.NextToken() return parser.curTok } func (parser *Parser) parseReference(startingSection *Section, period bool) (Value, error) { name := "" if period == false { name = parser.curTok.Literal } for { parser.readToken() if parser.curTok.ID == token.PERIOD && period == false { period = true } else if period && parser.curTok.ID == token.IDENTIFIER { if len(name) > 0 { name += "." } name += parser.curTok.Literal period = false } else if parser.curTok.ID == token.SEMICOLON { break } else { msg := fmt.Sprintf("expected ';' instead found '%s'", parser.curTok.Literal) return nil, parser.syntaxError(msg) } } if len(name) == 0 { return nil, parser.syntaxError( fmt.Sprintf("expected IDENTIFIER instead found %s", parser.curTok.Literal), ) } if period { return nil, parser.syntaxError(fmt.Sprintf("expected IDENTIFIER after PERIOD")) } return NewReference(name, startingSection), nil } func (parser *Parser) parseSetting(name string) error { var value Value parser.readToken() readNext := true switch parser.curTok.ID { case token.STRING: value = NewString(parser.curTok.Literal) case token.BOOLEAN: boolVal, err := strconv.ParseBool(parser.curTok.Literal) if err != nil { return nil } value = NewBoolean(boolVal) case token.NULL: value = NewNull() case token.INTEGER: intVal, err := strconv.ParseInt(parser.curTok.Literal, 10, 64) if err != nil { return err } value = NewInteger(intVal) case token.FLOAT: floatVal, err := strconv.ParseFloat(parser.curTok.Literal, 64) if err != nil { return err } value = NewFloat(floatVal) case token.PERIOD: reference, err := parser.parseReference(parser.curSection, true) if err != nil { return err } value = reference readNext = false case token.IDENTIFIER: reference, err := parser.parseReference(parser.settings, false) if err != nil { return err } value = reference readNext = false default: return parser.syntaxError( fmt.Sprintf("expected STRING, INTEGER, FLOAT, BOOLEAN or IDENTIFIER, instead found %s", parser.curTok.ID), ) } if readNext { parser.readToken() } if parser.curTok.ID != token.SEMICOLON { msg := fmt.Sprintf("expected ';' instead found '%s'", parser.curTok.Literal) return parser.syntaxError(msg) } parser.readToken() parser.curSection.Set(name, value) return nil } func (parser *Parser) parseInclude() error { if parser.curTok.ID != token.STRING { msg := fmt.Sprintf("expected STRING instead found '%s'", parser.curTok.ID) return parser.syntaxError(msg) } pattern := parser.curTok.Literal parser.readToken() if parser.curTok.ID != token.SEMICOLON { msg := fmt.Sprintf("expected ';' instead found '%s'", parser.curTok.Literal) return parser.syntaxError(msg) } filenames, err := filepath.Glob(pattern) if err != nil { return err } oldScanner := parser.scanner for _, filename := range filenames { // We have already visited this file, don't include again // DEV: This can cause recursive includes if this isn't here :o if parser.hasParsed(filename) { continue } reader, err := os.Open(filename) defer reader.Close() if err != nil { return err } parser.curSection.AddInclude(filename) parser.scanner = NewScanner(reader) parser.parse() // Make sure to add the filename to the internal list to ensure we don't // accidentally recursively include config files parser.addFile(filename) } parser.scanner = oldScanner parser.readToken() return nil } func (parser *Parser) parseSection(name string) error { section := parser.curSection.AddSection(name) parser.previous = append(parser.previous, parser.curSection) parser.curSection = section return nil } func (parser *Parser) endSection() error { if len(parser.previous) == 0 { return parser.syntaxError("unexpected section end '}'") } pLen := len(parser.previous) previous := parser.previous[pLen-1] parser.previous = parser.previous[0 : pLen-1] parser.curSection = previous return nil } func (parser *Parser) parse() error { parser.readToken() for { if parser.curTok.ID == token.EOF { break } tok := parser.curTok parser.readToken() switch tok.ID { case token.COMMENT: parser.curSection.AddComment(tok.Literal) case token.INCLUDE: parser.parseInclude() case token.IDENTIFIER: if parser.curTok.ID == token.LBRACKET { err := parser.parseSection(tok.Literal) if err != nil { return err } parser.readToken() } else if parser.curTok.ID == token.EQUAL { err := parser.parseSetting(tok.Literal) if err != nil { return err } } case token.RBRACKET: err := parser.endSection() if err != nil { return err } default: return parser.syntaxError(fmt.Sprintf("unexpected token %s", tok)) } } return nil } // GetSettings will fetch the parsed settings from this Parser func (parser *Parser) GetSettings() *Section { return parser.settings } // Parse will tell the Parser to parse all settings from the config func (parser *Parser) Parse() error { err := parser.parse() if err != nil { return err } if len(parser.previous) > 0 { return parser.syntaxError("expected end of section, instead found EOF") } return nil }