The beginning of something
This commit is contained in:
parent
83c32989a6
commit
b6a1162606
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
y.go
|
||||
y.output
|
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
||||
default: test
|
||||
|
||||
test: y.go
|
||||
go test
|
||||
|
||||
y.go: parse.y
|
||||
go tool yacc -p "hcl" parse.y
|
||||
|
||||
.PHONY: default test
|
4
hcl_test.go
Normal file
4
hcl_test.go
Normal file
@ -0,0 +1,4 @@
|
||||
package hcl
|
||||
|
||||
// This is the directory where our test fixtures are.
|
||||
const fixtureDir = "./test-fixtures"
|
246
lex.go
Normal file
246
lex.go
Normal file
@ -0,0 +1,246 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// The parser expects the lexer to return 0 on EOF.
|
||||
const lexEOF = 0
|
||||
|
||||
// The parser uses the type <prefix>Lex as a lexer. It must provide
|
||||
// the methods Lex(*<prefix>SymType) int and Error(string).
|
||||
type hclLex struct {
|
||||
Input string
|
||||
|
||||
pos int
|
||||
width int
|
||||
col, line int
|
||||
err error
|
||||
}
|
||||
|
||||
// The parser calls this method to get each new token.
|
||||
func (x *hclLex) Lex(yylval *hclSymType) int {
|
||||
for {
|
||||
c := x.next()
|
||||
if c == lexEOF {
|
||||
return lexEOF
|
||||
}
|
||||
|
||||
// Ignore all whitespace
|
||||
if unicode.IsSpace(c) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If it is a number, lex the number
|
||||
if c >= '0' && c <= '9' {
|
||||
x.backup()
|
||||
return x.lexNumber(yylval)
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '=':
|
||||
return EQUAL
|
||||
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:
|
||||
x.backup()
|
||||
return x.lexId(yylval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (x *hclLex) consumeComment(c rune) bool {
|
||||
single := c == '#'
|
||||
if !single {
|
||||
c = x.next()
|
||||
if c != '/' && c != '*' {
|
||||
x.backup()
|
||||
x.createErr(fmt.Sprintf("comment expected, got '%c'", c))
|
||||
return false
|
||||
}
|
||||
|
||||
single = c == '/'
|
||||
}
|
||||
|
||||
nested := 1
|
||||
for {
|
||||
c = x.next()
|
||||
if c == lexEOF {
|
||||
x.backup()
|
||||
return true
|
||||
}
|
||||
|
||||
// Single line comments continue until a '\n'
|
||||
if single {
|
||||
if c == '\n' {
|
||||
return true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Multi-line comments continue until a '*/'
|
||||
switch c {
|
||||
case '/':
|
||||
c = x.next()
|
||||
if c == '*' {
|
||||
nested++
|
||||
} else {
|
||||
x.backup()
|
||||
}
|
||||
case '*':
|
||||
c = x.next()
|
||||
if c == '/' {
|
||||
nested--
|
||||
} else {
|
||||
x.backup()
|
||||
}
|
||||
default:
|
||||
// Continue
|
||||
}
|
||||
|
||||
// If we're done with the comment, return!
|
||||
if nested == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lexId lexes an identifier
|
||||
func (x *hclLex) lexId(yylval *hclSymType) 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 != '.' &&
|
||||
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
|
||||
}
|
||||
|
||||
// lexNumber lexes out a number
|
||||
func (x *hclLex) lexNumber(yylval *hclSymType) int {
|
||||
var b bytes.Buffer
|
||||
for {
|
||||
c := x.next()
|
||||
if c == lexEOF {
|
||||
break
|
||||
}
|
||||
|
||||
// No more numeric characters
|
||||
if c < '0' || c > '9' {
|
||||
x.backup()
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := b.WriteRune(c); err != nil {
|
||||
x.createErr(fmt.Sprintf("Internal error: %s", err))
|
||||
return lexEOF
|
||||
}
|
||||
}
|
||||
|
||||
v, err := strconv.ParseInt(b.String(), 0, 0)
|
||||
if err != nil {
|
||||
x.createErr(fmt.Sprintf("Expected number: %s", err))
|
||||
return lexEOF
|
||||
}
|
||||
|
||||
yylval.num = int(v)
|
||||
return NUMBER
|
||||
}
|
||||
|
||||
// lexString extracts a string from the input
|
||||
func (x *hclLex) lexString(yylval *hclSymType) 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 *hclLex) 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 *hclLex) peek() rune {
|
||||
r := x.next()
|
||||
x.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per next.
|
||||
func (x *hclLex) backup() {
|
||||
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)
|
||||
}
|
||||
|
||||
// The parser calls this method on a parse error.
|
||||
func (x *hclLex) Error(s string) {
|
||||
log.Printf("parse error: %s", s)
|
||||
}
|
54
lex_test.go
Normal file
54
lex_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output []int
|
||||
}{
|
||||
{
|
||||
"comment.hcl",
|
||||
[]int{IDENTIFIER, EQUAL, STRING, lexEOF},
|
||||
},
|
||||
{
|
||||
"structure.hcl",
|
||||
[]int{
|
||||
IDENTIFIER, IDENTIFIER, STRING, LEFTBRACE,
|
||||
IDENTIFIER, EQUAL, NUMBER, SEMICOLON,
|
||||
RIGHTBRACE, lexEOF,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.Input))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
l := &hclLex{Input: string(d)}
|
||||
var actual []int
|
||||
for {
|
||||
token := l.Lex(new(hclSymType))
|
||||
actual = append(actual, token)
|
||||
|
||||
if token == lexEOF {
|
||||
break
|
||||
}
|
||||
|
||||
if len(actual) > 500 {
|
||||
t.Fatalf("Input:%s\n\nExausted.", tc.Input)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.Output) {
|
||||
t.Fatalf("Input: %s\n\nBad: %#v", tc.Input, actual)
|
||||
}
|
||||
}
|
||||
}
|
34
parse.go
Normal file
34
parse.go
Normal file
@ -0,0 +1,34 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// exprErrors are the errors built up from parsing. These should not
|
||||
// be accessed directly.
|
||||
var exprErrors []error
|
||||
var exprLock sync.Mutex
|
||||
var exprResult []map[string]interface{}
|
||||
|
||||
/*
|
||||
// ExprParse parses the given expression and returns an executable
|
||||
// Interpolation.
|
||||
func ExprParse(v string) (Interpolation, error) {
|
||||
exprLock.Lock()
|
||||
defer exprLock.Unlock()
|
||||
exprErrors = nil
|
||||
exprResult = nil
|
||||
|
||||
// Parse
|
||||
exprParse(&exprLex{input: v})
|
||||
|
||||
// Build up the errors
|
||||
var err error
|
||||
if len(exprErrors) > 0 {
|
||||
err = &multierror.Error{Errors: exprErrors}
|
||||
exprResult = nil
|
||||
}
|
||||
|
||||
return exprResult, err
|
||||
}
|
||||
*/
|
34
parse.y
Normal file
34
parse.y
Normal file
@ -0,0 +1,34 @@
|
||||
// This is the yacc input for creating the parser for HCL.
|
||||
|
||||
%{
|
||||
package hcl
|
||||
|
||||
%}
|
||||
|
||||
%union {
|
||||
num int
|
||||
obj map[string]interface{}
|
||||
str string
|
||||
}
|
||||
|
||||
%type <obj> object
|
||||
|
||||
%token <num> NUMBER
|
||||
%token <str> IDENTIFIER EQUAL SEMICOLON STRING
|
||||
%token <str> LEFTBRACE RIGHTBRACE
|
||||
|
||||
%%
|
||||
|
||||
top:
|
||||
object
|
||||
{
|
||||
exprResult = []map[string]interface{}{$1}
|
||||
}
|
||||
|
||||
object:
|
||||
IDENTIFIER EQUAL STRING
|
||||
{
|
||||
$$ = map[string]interface{}{$1: $3}
|
||||
}
|
||||
|
||||
%%
|
16
test-fixtures/comment.hcl
Normal file
16
test-fixtures/comment.hcl
Normal file
@ -0,0 +1,16 @@
|
||||
// Foo
|
||||
|
||||
/* Bar */
|
||||
|
||||
/*
|
||||
/*
|
||||
Baz
|
||||
*/
|
||||
*/
|
||||
|
||||
# Another
|
||||
|
||||
# Multiple
|
||||
# Lines
|
||||
|
||||
foo = "bar"
|
4
test-fixtures/structure.hcl
Normal file
4
test-fixtures/structure.hcl
Normal file
@ -0,0 +1,4 @@
|
||||
// This is a test structure for the lexer
|
||||
foo bar "baz" {
|
||||
key = 7;
|
||||
}
|
Loading…
Reference in New Issue
Block a user