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
}
// Ignore all whitespace
// Ignore all whitespace except a newline which we handle
// specially later.
if unicode.IsSpace(c) {
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 c >= '0' && c <= '9' {
x.backup()
@ -43,21 +56,18 @@ func (x *hclLex) Lex(yylval *hclSymType) int {
}
switch c {
case ',':
return COMMA
case '=':
return EQUAL
case '[':
return LEFTBRACKET
case ']':
return RIGHTBRACKET
case '{':
return LEFTBRACE
case '}':
return RIGHTBRACE
case ';':
return SEMICOLON
case '#':
fallthrough
case '/':
// Starting comment
if !x.consumeComment(c) {
return lexEOF
}
case '"':
return x.lexString(yylval)
default:
@ -220,6 +230,16 @@ func (x *hclLex) next() rune {
r, w := utf8.DecodeRuneInString(x.Input[x.pos:])
x.width = w
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
}
@ -232,15 +252,17 @@ func (x *hclLex) peek() rune {
// backup steps back one rune. Can only be called once per next.
func (x *hclLex) backup() {
x.col -= 1
x.pos -= x.width
}
// createErr records the given error
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.
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",
[]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",
[]int{
@ -28,7 +44,8 @@ func TestLex(t *testing.T) {
"structure.hcl",
[]int{
IDENTIFIER, IDENTIFIER, STRING, LEFTBRACE,
IDENTIFIER, EQUAL, NUMBER, SEMICOLON,
IDENTIFIER, EQUAL, NUMBER,
IDENTIFIER, EQUAL, STRING,
RIGHTBRACE, lexEOF,
},
},
@ -56,7 +73,9 @@ func TestLex(t *testing.T) {
}
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.
var hclErrors []error
var hclLock sync.Mutex
var hclResult map[string]interface{}
var hclResult *ObjectNode
// 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()
defer hclLock.Unlock()
hclErrors = nil

105
parse.y
View File

@ -6,38 +6,83 @@ package hcl
%}
%union {
list []Node
listitem Node
num int
obj map[string]interface{}
obj ObjectNode
str string
}
%type <obj> block object
%type <list> list
%type <listitem> listitem
%type <obj> block object objectlist
%type <str> blockId
%token <num> NUMBER
%token <str> IDENTIFIER EQUAL SEMICOLON STRING
%token <str> LEFTBRACE RIGHTBRACE
%token <str> COMMA IDENTIFIER EQUAL NEWLINE STRING
%token <str> LEFTBRACE RIGHTBRACE LEFTBRACKET RIGHTBRACKET
%%
top:
object
objectlist
{
hclResult = $1
hclResult = &ObjectNode{
Elem: $1.Elem,
}
}
object:
object SEMICOLON
objectlist:
object
{
$$ = $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
{
$$ = 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
{
@ -45,13 +90,21 @@ object:
}
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
{
$$ = map[string]interface{}{$1: []interface{}{$2}}
$$ = ObjectNode{
Elem: map[string][]Node{
$1: []Node{$2},
},
}
}
blockId:
@ -64,4 +117,28 @@ blockId:
$$ = $1
}
list:
listitem
{
$$ = []Node{$1}
}
| list COMMA listitem
{
$$ = append($1, $3)
}
listitem:
object
{
$$ = $1
}
| NUMBER
{
$$ = $1
}
| STRING
{
$$ = $1
}
%%

View File

@ -10,34 +10,51 @@ import (
func TestParse(t *testing.T) {
cases := []struct {
Input string
Output map[string]interface{}
Output *ObjectNode
}{
{
"comment.hcl",
map[string]interface{}{
"foo": []interface{}{"bar"},
},
},
{
"structure_basic.hcl",
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"value": []interface{}{7},
&ObjectNode{
Elem: map[string][]Node{
"foo": []Node{
ValueNode{
Type: ValueTypeString,
Value: "bar",
},
},
},
},
},
{
"structure.hcl",
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": []interface{}{
map[string]interface{}{
"baz": []interface{}{
map[string]interface{}{
"key": []interface{}{7},
"multiple.hcl",
&ObjectNode{
Elem: map[string][]Node{
"foo": []Node{
ValueNode{
Type: ValueTypeString,
Value: "bar",
},
},
"key": []Node{
ValueNode{
Type: ValueTypeInt,
Value: 7,
},
},
},
},
},
{
"structure_basic.hcl",
&ObjectNode{
Elem: map[string][]Node{
"foo": []Node{
ObjectNode{
Elem: map[string][]Node{
"value": []Node{
ValueNode{
Type: ValueTypeInt,
Value: 7,
},
},
},
@ -46,6 +63,48 @@ func TestParse(t *testing.T) {
},
},
},
{
"structure.hcl",
&ObjectNode{
Elem: map[string][]Node{
"foo": []Node{
ObjectNode{
Elem: map[string][]Node{
"bar": []Node{
ObjectNode{
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",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
"complex.hcl",
nil,
},
}
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
foo bar "baz" {
key = 7;
key = 7
foo = "bar"
}