package parser import ( "os/user" "path/filepath" "strings" "github.com/kashav/fsql/query" "github.com/kashav/fsql/tokenizer" ) // Run parses the input string and returns the parsed AST (query). func Run(input string) (*query.Query, error) { return (&parser{}).parse(input) } type parser struct { tokenizer *tokenizer.Tokenizer current *tokenizer.Token expected tokenizer.TokenType } // parse runs the respective parser function on each clause of the query. func (p *parser) parse(input string) (*query.Query, error) { q := query.NewQuery() p.tokenizer = tokenizer.NewTokenizer(input) if err := p.parseSelectClause(q); err != nil { return nil, err } if err := p.parseFromClause(q); err != nil { return nil, err } if err := p.parseWhereClause(q); err != nil { return nil, err } return q, nil } // parseSelectClause parses the SELECT clause of the query. func (p *parser) parseSelectClause(q *query.Query) error { // Determine if we should show all attributes. This is only true when // no attributes are provided (regardless of if the SELECT keyword is // provided or not). var showAll = true if p.expect(tokenizer.Select) == nil { if p.current == nil || p.current.Type == tokenizer.Identifier { showAll = false } else if p.current.Type == tokenizer.From || p.current.Type == tokenizer.Where { // No SELECT and next token is FROM/WHERE, show all! showAll = true } else { // No SELECT and next token is not Identifier nor FROM/WHERE -> malformed // input. return p.currentError() } } else if current := p.expect(tokenizer.Identifier); current != nil { p.current = current showAll = false } if showAll { q.Attributes = allAttributes } else if err := p.parseAttrs(&q.Attributes, &q.Modifiers); err != nil { return err } return nil } // parseFromClause parses the FROM clause of the query. func (p *parser) parseFromClause(q *query.Query) error { if p.expect(tokenizer.From) == nil { err := p.currentError() if p.expect(tokenizer.Identifier) != nil { // No FROM, but an identifier -> malformed query. return err } // No specified directory, so we default to the CWD. q.Sources["include"] = append(q.Sources["include"], ".") return nil } if err := p.parseSourceList(&q.Sources, &q.SourceAliases); err != nil { return err } // Replace the tilde with the home directory in each source directory. This // is only required when the query is wrapped in quotes, since the shell // will automatically expand tildes otherwise. u, err := user.Current() if err != nil { return err } for _, sourceType := range []string{"include", "exclude"} { for i, src := range q.Sources[sourceType] { if strings.Contains(src, "~") { q.Sources[sourceType][i] = filepath.Join(u.HomeDir, src[1:]) } } } return nil } // parseWhereClause parses the WHERE clause of the query. func (p *parser) parseWhereClause(q *query.Query) error { if p.expect(tokenizer.Where) == nil { err := p.currentError() if p.expect(tokenizer.Identifier) == nil { return nil } return err } root, err := p.parseConditionTree() if err != nil { return err } q.ConditionTree = root return nil } // expect returns the next token if it matches the expectation t, and // nil otherwise. func (p *parser) expect(t tokenizer.TokenType) *tokenizer.Token { p.expected = t if p.current == nil { p.current = p.tokenizer.Next() } if p.current != nil && p.current.Type == t { tok := p.current p.current = nil return tok } return nil }