From 1dcefba5c439daf12d9ba9a1a98b9d7997dee5ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Jul 2014 15:23:01 -0700 Subject: [PATCH] config: parser --- .gitignore | 2 + config/expr.y | 84 +++++++++++++++++++++++++++ config/expr_lex.go | 130 ++++++++++++++++++++++++++++++++++++++++++ config/expr_test.go | 9 +++ config/interpolate.go | 4 +- 5 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 config/expr.y create mode 100644 config/expr_lex.go create mode 100644 config/expr_test.go diff --git a/.gitignore b/.gitignore index 03a5621e4..e7669ef01 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ example.tf terraform.tfplan terraform.tfstate bin/ +config/y.go +config/y.output vendor/ diff --git a/config/expr.y b/config/expr.y new file mode 100644 index 000000000..58e5fac67 --- /dev/null +++ b/config/expr.y @@ -0,0 +1,84 @@ +// This is the yacc input for creating the parser for interpolation +// expressions in Go. + +// To build it: +// +// go tool yacc -p "expr" expr.y (produces y.go) +// + +%{ +package config + +import ( + "fmt" +) + +%} + +%union { + expr Interpolation + str string + variable InterpolatedVariable + args []Interpolation +} + +%type args +%type expr +%type string +%type variable + +%token STRING IDENTIFIER +%token COMMA LEFTPAREN RIGHTPAREN + +%% + +top: + expr + { + fmt.Printf("%#v", $1) + } + +expr: + string + { + $$ = &LiteralInterpolation{Literal: $1} + } +| variable + { + $$ = &VariableInterpolation{Variable: $1} + } +| IDENTIFIER LEFTPAREN args RIGHTPAREN + { + $$ = &FunctionInterpolation{Func: $1, Args: $3} + } + +args: + { + $$ = nil + } +| expr COMMA expr + { + $$ = append($$, $1, $3) + } +| expr + { + $$ = append($$, $1) + } + +string: + STRING + { + $$ = $1 + } + +variable: + IDENTIFIER + { + var err error + $$, err = NewInterpolatedVariable($1) + if err != nil { + panic(err) + } + } + +%% diff --git a/config/expr_lex.go b/config/expr_lex.go new file mode 100644 index 000000000..5e8404431 --- /dev/null +++ b/config/expr_lex.go @@ -0,0 +1,130 @@ +package config + +import ( + "bytes" + "log" + "unicode" + "unicode/utf8" +) + +// The parser expects the lexer to return 0 on EOF. +const lexEOF = 0 + +// The parser uses the type Lex as a lexer. It must provide +// the methods Lex(*SymType) int and Error(string). +type exprLex struct { + input string + pos int + width int +} + +// The parser calls this method to get each new token. +func (x *exprLex) Lex(yylval *exprSymType) int { + for { + c := x.next() + if c == lexEOF { + return lexEOF + } + + // Ignore all whitespace + if unicode.IsSpace(c) { + continue + } + + switch c { + case '"': + return x.lexString(yylval) + case ',': + return COMMA + case '(': + return LEFTPAREN + case ')': + return RIGHTPAREN + default: + x.backup() + return x.lexId(yylval) + } + } +} + +func (x *exprLex) lexId(yylval *exprSymType) int { + var b bytes.Buffer + for { + c := x.next() + if c == lexEOF { + break + } + + // If this isn't a character we want in an ID, return out. + // One day we should make this a regexp. + if c != '_' && + c != '.' && + c != '*' && + !unicode.IsLetter(c) && + !unicode.IsNumber(c) { + x.backup() + break + } + + if _, err := b.WriteRune(c); err != nil { + log.Printf("ERR: %s", err) + return lexEOF + } + } + + yylval.str = b.String() + return IDENTIFIER +} + +func (x *exprLex) lexString(yylval *exprSymType) int { + var b bytes.Buffer + for { + c := x.next() + if c == lexEOF { + break + } + + // String end + if c == '"' { + break + } + + if _, err := b.WriteRune(c); err != nil { + log.Printf("ERR: %s", err) + return lexEOF + } + } + + yylval.str = b.String() + return STRING +} + +// Return the next rune for the lexer. +func (x *exprLex) next() rune { + if int(x.pos) >= len(x.input) { + x.width = 0 + return lexEOF + } + + r, w := utf8.DecodeRuneInString(x.input[x.pos:]) + x.width = w + x.pos += x.width + return r +} + +// peek returns but does not consume the next rune in the input +func (x *exprLex) peek() rune { + r := x.next() + x.backup() + return r +} + +// backup steps back one rune. Can only be called once per next. +func (x *exprLex) backup() { + x.pos -= x.width +} + +// The parser calls this method on a parse error. +func (x *exprLex) Error(s string) { + log.Printf("parse error: %s", s) +} diff --git a/config/expr_test.go b/config/expr_test.go new file mode 100644 index 000000000..68a6b5fd4 --- /dev/null +++ b/config/expr_test.go @@ -0,0 +1,9 @@ +package config + +import ( + "testing" +) + +func TestExprParse(t *testing.T) { + exprParse(&exprLex{input: `lookup(var.foo)`}) +} diff --git a/config/interpolate.go b/config/interpolate.go index 935ff2e8f..64cb90df0 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -37,8 +37,8 @@ type InterpolatedVariable interface { // FunctionInterpolation is an Interpolation that executes a function // with some variable number of arguments to generate a value. type FunctionInterpolation struct { - Func InterpolationFunc - Args []InterpolatedVariable + Func string + Args []Interpolation key string }