Commit things

This commit is contained in:
Mitchell Hashimoto 2014-07-31 18:44:21 -07:00
parent 8e04dbf597
commit 9a98eac129
10 changed files with 294 additions and 51 deletions

20
ast.go Normal file
View File

@ -0,0 +1,20 @@
package hcl
type ValueType byte
const (
ValueTypeUnknown ValueType = iota
ValueTypeInt
ValueTypeString
)
type Node interface{}
type ObjectNode struct {
Elem map[string][]Node
}
type ValueNode struct {
Type ValueType
Value interface{}
}

46
lex.go
View File

@ -31,11 +31,24 @@ func (x *hclLex) Lex(yylval *hclSymType) int {
return lexEOF return lexEOF
} }
// Ignore all whitespace // Ignore all whitespace except a newline which we handle
// specially later.
if unicode.IsSpace(c) { if unicode.IsSpace(c) {
continue continue
} }
// Consume all comments
switch c {
case '#':
fallthrough
case '/':
// Starting comment
if !x.consumeComment(c) {
return lexEOF
}
continue
}
// If it is a number, lex the number // If it is a number, lex the number
if c >= '0' && c <= '9' { if c >= '0' && c <= '9' {
x.backup() x.backup()
@ -43,21 +56,18 @@ func (x *hclLex) Lex(yylval *hclSymType) int {
} }
switch c { switch c {
case ',':
return COMMA
case '=': case '=':
return EQUAL return EQUAL
case '[':
return LEFTBRACKET
case ']':
return RIGHTBRACKET
case '{': case '{':
return LEFTBRACE return LEFTBRACE
case '}': case '}':
return RIGHTBRACE return RIGHTBRACE
case ';':
return SEMICOLON
case '#':
fallthrough
case '/':
// Starting comment
if !x.consumeComment(c) {
return lexEOF
}
case '"': case '"':
return x.lexString(yylval) return x.lexString(yylval)
default: default:
@ -220,6 +230,16 @@ func (x *hclLex) next() rune {
r, w := utf8.DecodeRuneInString(x.Input[x.pos:]) r, w := utf8.DecodeRuneInString(x.Input[x.pos:])
x.width = w x.width = w
x.pos += x.width x.pos += x.width
x.col += 1
if x.line == 0 {
x.line = 1
}
if r == '\n' {
x.line += 1
x.col = 0
}
return r return r
} }
@ -232,15 +252,17 @@ func (x *hclLex) peek() rune {
// backup steps back one rune. Can only be called once per next. // backup steps back one rune. Can only be called once per next.
func (x *hclLex) backup() { func (x *hclLex) backup() {
x.col -= 1
x.pos -= x.width x.pos -= x.width
} }
// createErr records the given error // createErr records the given error
func (x *hclLex) createErr(msg string) { func (x *hclLex) createErr(msg string) {
x.err = fmt.Errorf("Line %d, column %d: %s", x.col, x.line, msg) x.err = fmt.Errorf("Line %d, column %d: %s", x.line, x.col, msg)
log.Printf("parse error: %s", x.err)
} }
// The parser calls this method on a parse error. // The parser calls this method on a parse error.
func (x *hclLex) Error(s string) { func (x *hclLex) Error(s string) {
log.Printf("parse error: %s", s) x.createErr(s)
} }

View File

@ -16,6 +16,22 @@ func TestLex(t *testing.T) {
"comment.hcl", "comment.hcl",
[]int{IDENTIFIER, EQUAL, STRING, lexEOF}, []int{IDENTIFIER, EQUAL, STRING, lexEOF},
}, },
{
"multiple.hcl",
[]int{
IDENTIFIER, EQUAL, STRING,
IDENTIFIER, EQUAL, NUMBER,
lexEOF,
},
},
{
"list.hcl",
[]int{
IDENTIFIER, EQUAL, LEFTBRACKET,
NUMBER, COMMA, NUMBER, COMMA, STRING,
RIGHTBRACKET, lexEOF,
},
},
{ {
"structure_basic.hcl", "structure_basic.hcl",
[]int{ []int{
@ -28,7 +44,8 @@ func TestLex(t *testing.T) {
"structure.hcl", "structure.hcl",
[]int{ []int{
IDENTIFIER, IDENTIFIER, STRING, LEFTBRACE, IDENTIFIER, IDENTIFIER, STRING, LEFTBRACE,
IDENTIFIER, EQUAL, NUMBER, SEMICOLON, IDENTIFIER, EQUAL, NUMBER,
IDENTIFIER, EQUAL, STRING,
RIGHTBRACE, lexEOF, RIGHTBRACE, lexEOF,
}, },
}, },
@ -56,7 +73,9 @@ func TestLex(t *testing.T) {
} }
if !reflect.DeepEqual(actual, tc.Output) { if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("Input: %s\n\nBad: %#v", tc.Input, actual) t.Fatalf(
"Input: %s\n\nBad: %#v\n\nExpected: %#v",
tc.Input, actual, tc.Output)
} }
} }
} }

View File

@ -10,10 +10,10 @@ import (
// be accessed directly. // be accessed directly.
var hclErrors []error var hclErrors []error
var hclLock sync.Mutex var hclLock sync.Mutex
var hclResult map[string]interface{} var hclResult *ObjectNode
// Parse parses the given string and returns the result. // Parse parses the given string and returns the result.
func Parse(v string) (map[string]interface{}, error) { func Parse(v string) (*ObjectNode, error) {
hclLock.Lock() hclLock.Lock()
defer hclLock.Unlock() defer hclLock.Unlock()
hclErrors = nil hclErrors = nil

105
parse.y
View File

@ -6,38 +6,83 @@ package hcl
%} %}
%union { %union {
list []Node
listitem Node
num int num int
obj map[string]interface{} obj ObjectNode
str string str string
} }
%type <obj> block object %type <list> list
%type <listitem> listitem
%type <obj> block object objectlist
%type <str> blockId %type <str> blockId
%token <num> NUMBER %token <num> NUMBER
%token <str> IDENTIFIER EQUAL SEMICOLON STRING %token <str> COMMA IDENTIFIER EQUAL NEWLINE STRING
%token <str> LEFTBRACE RIGHTBRACE %token <str> LEFTBRACE RIGHTBRACE LEFTBRACKET RIGHTBRACKET
%% %%
top: top:
object objectlist
{ {
hclResult = $1 hclResult = &ObjectNode{
Elem: $1.Elem,
}
} }
object: objectlist:
object SEMICOLON object
{ {
$$ = $1 $$ = $1
} }
| IDENTIFIER EQUAL NUMBER | object objectlist
{ {
$$ = map[string]interface{}{$1: []interface{}{$3}} $$ = $1
for k, v := range $2.Elem {
if _, ok := $$.Elem[k]; ok {
$$.Elem[k] = append($$.Elem[k], v...)
} else {
$$.Elem[k] = v
}
}
}
object:
IDENTIFIER EQUAL NUMBER
{
$$ = ObjectNode{
Elem: map[string][]Node{
$1: []Node{
ValueNode{
Type: ValueTypeInt,
Value: $3,
},
},
},
}
} }
| IDENTIFIER EQUAL STRING | IDENTIFIER EQUAL STRING
{ {
$$ = map[string]interface{}{$1: []interface{}{$3}} $$ = ObjectNode{
Elem: map[string][]Node{
$1: []Node{
ValueNode{
Type: ValueTypeString,
Value: $3,
},
},
},
}
}
| IDENTIFIER EQUAL LEFTBRACKET list RIGHTBRACKET
{
$$ = ObjectNode{
Elem: map[string][]Node{
$1: $4,
},
}
} }
| block | block
{ {
@ -45,13 +90,21 @@ object:
} }
block: block:
blockId LEFTBRACE object RIGHTBRACE blockId LEFTBRACE objectlist RIGHTBRACE
{ {
$$ = map[string]interface{}{$1: []interface{}{$3}} $$ = ObjectNode{
Elem: map[string][]Node{
$1: []Node{$3},
},
}
} }
| blockId block | blockId block
{ {
$$ = map[string]interface{}{$1: []interface{}{$2}} $$ = ObjectNode{
Elem: map[string][]Node{
$1: []Node{$2},
},
}
} }
blockId: blockId:
@ -64,4 +117,28 @@ blockId:
$$ = $1 $$ = $1
} }
list:
listitem
{
$$ = []Node{$1}
}
| list COMMA listitem
{
$$ = append($1, $3)
}
listitem:
object
{
$$ = $1
}
| NUMBER
{
$$ = $1
}
| STRING
{
$$ = $1
}
%% %%

View File

@ -10,34 +10,83 @@ import (
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
cases := []struct { cases := []struct {
Input string Input string
Output map[string]interface{} Output *ObjectNode
}{ }{
{ {
"comment.hcl", "comment.hcl",
map[string]interface{}{ &ObjectNode{
"foo": []interface{}{"bar"}, Elem: map[string][]Node{
"foo": []Node{
ValueNode{
Type: ValueTypeString,
Value: "bar",
},
},
},
},
},
{
"multiple.hcl",
&ObjectNode{
Elem: map[string][]Node{
"foo": []Node{
ValueNode{
Type: ValueTypeString,
Value: "bar",
},
},
"key": []Node{
ValueNode{
Type: ValueTypeInt,
Value: 7,
},
},
},
}, },
}, },
{ {
"structure_basic.hcl", "structure_basic.hcl",
map[string]interface{}{ &ObjectNode{
"foo": []interface{}{ Elem: map[string][]Node{
map[string]interface{}{ "foo": []Node{
"value": []interface{}{7}, ObjectNode{
Elem: map[string][]Node{
"value": []Node{
ValueNode{
Type: ValueTypeInt,
Value: 7,
},
},
},
},
}, },
}, },
}, },
}, },
{ {
"structure.hcl", "structure.hcl",
map[string]interface{}{ &ObjectNode{
"foo": []interface{}{ Elem: map[string][]Node{
map[string]interface{}{ "foo": []Node{
"bar": []interface{}{ ObjectNode{
map[string]interface{}{ Elem: map[string][]Node{
"baz": []interface{}{ "bar": []Node{
map[string]interface{}{ ObjectNode{
"key": []interface{}{7}, Elem: map[string][]Node{
"baz": []Node{
ObjectNode{
Elem: map[string][]Node{
"key": []Node{
ValueNode{
Type: ValueTypeInt,
Value: 7,
},
},
"foo": []Node{
ValueNode{
Type: ValueTypeString,
Value: "bar",
}, },
}, },
}, },
@ -46,6 +95,16 @@ func TestParse(t *testing.T) {
}, },
}, },
}, },
},
},
},
},
},
},
{
"complex.hcl",
nil,
},
} }
for _, tc := range cases { for _, tc := range cases {

42
test-fixtures/complex.hcl Normal file
View File

@ -0,0 +1,42 @@
// This comes from Terraform, as a test
variable "foo" {
default = "bar"
description = "bar"
}
provider "aws" {
access_key = "foo"
secret_key = "bar"
}
provider "do" {
api_key = "${var.foo}"
}
resource "aws_security_group" "firewall" {
count = 5
}
resource aws_instance "web" {
ami = "${var.foo}"
security_groups = [
"foo",
"${aws_security_group.firewall.foo}"
]
network_interface {
device_index = 0
description = "Main network interface"
}
}
resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}"
VPC = "foo"
depends_on = ["aws_instance.web"]
}
output "web_ip" {
value = "${aws_instance.web.private_ip}"
}

1
test-fixtures/list.hcl Normal file
View File

@ -0,0 +1 @@
foo = [1, 2, "foo"]

View File

@ -0,0 +1,2 @@
foo = "bar"
key = 7

View File

@ -1,4 +1,5 @@
// This is a test structure for the lexer // This is a test structure for the lexer
foo bar "baz" { foo bar "baz" {
key = 7; key = 7
foo = "bar"
} }