diff --git a/command/build.go b/command/build.go index 43345f6e1..acf568b1e 100644 --- a/command/build.go +++ b/command/build.go @@ -39,11 +39,7 @@ func (c BuildCommand) Run(args []string) int { // Parse the template var tpl *template.Template var err error - if args[0] == "-" { - tpl, err = template.Parse(os.Stdin) - } else { - tpl, err = template.ParseFile(args[0]) - } + tpl, err = template.ParseFile(args[0]) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err)) return 1 diff --git a/template/parse.go b/template/parse.go index e5d7d3dc5..bb17b1bd8 100644 --- a/template/parse.go +++ b/template/parse.go @@ -1,10 +1,12 @@ package template import ( + "bufio" "bytes" "encoding/json" "fmt" "io" + "io/ioutil" "os" "path/filepath" "sort" @@ -309,17 +311,56 @@ func Parse(r io.Reader) (*Template, error) { return rawTpl.Template() } +// Find line number and position based on the offset +func findLinePos(f *os.File, offset int64) (int64, int64, string) { + scanner := bufio.NewScanner(f) + count := int64(0) + for scanner.Scan() { + count += 1 + scanLength := len(scanner.Text()) + 1 + if offset < int64(scanLength) { + return count, offset, scanner.Text() + } + offset = offset - int64(scanLength) + } + if err := scanner.Err(); err != nil { + return 0, 0, err.Error() + } + return 0, 0, "" +} + // ParseFile is the same as Parse but is a helper to automatically open // a file for parsing. func ParseFile(path string) (*Template, error) { - f, err := os.Open(path) - if err != nil { - return nil, err + var f *os.File + var err error + if path == "-" { + // Create a temp file for stdin in case of errors + f, err = ioutil.TempFile(os.TempDir(), "packer") + if err != nil { + return nil, err + } + defer os.Remove(f.Name()) + defer f.Close() + io.Copy(f, os.Stdin) + f.Seek(0, os.SEEK_SET) + } else { + f, err = os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() } - defer f.Close() - tpl, err := Parse(f) if err != nil { + syntaxErr, ok := err.(*json.SyntaxError) + if !ok { + return nil, err + } + // Rewind the file and get a better error + f.Seek(0, os.SEEK_SET) + line, pos, errorLine := findLinePos(f, syntaxErr.Offset) + err = fmt.Errorf("Error in line %d, char %d: %s\n%s", line, pos, syntaxErr, errorLine) return nil, err } diff --git a/template/parse_test.go b/template/parse_test.go index c2fd72742..008266642 100644 --- a/template/parse_test.go +++ b/template/parse_test.go @@ -350,3 +350,23 @@ func TestParse_contents(t *testing.T) { t.Fatalf("bad: %s\n\n%s", actual, expected) } } + +func TestParse_bad(t *testing.T) { + cases := []struct { + File string + Expected string + }{ + {"error-beginning.json", "line 1, char 1"}, + {"error-middle.json", "line 5, char 5"}, + {"error-end.json", "line 1, char 30"}, + } + for _, tc := range cases { + _, err := ParseFile(fixtureDir(tc.File)) + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), tc.Expected) { + t.Fatalf("file: %s\nExpected: %s\n%s\n", tc.File, tc.Expected, err.Error()) + } + } +} diff --git a/template/test-fixtures/error-beginning.json b/template/test-fixtures/error-beginning.json new file mode 100644 index 000000000..eb9a1aa6f --- /dev/null +++ b/template/test-fixtures/error-beginning.json @@ -0,0 +1 @@ +*"builders": [ { "type":"test", }]} diff --git a/template/test-fixtures/error-end.json b/template/test-fixtures/error-end.json new file mode 100644 index 000000000..95755d9eb --- /dev/null +++ b/template/test-fixtures/error-end.json @@ -0,0 +1 @@ +{"builders":[{"type":"test"}]* diff --git a/template/test-fixtures/error-middle.json b/template/test-fixtures/error-middle.json new file mode 100644 index 000000000..65cda6ea3 --- /dev/null +++ b/template/test-fixtures/error-middle.json @@ -0,0 +1,7 @@ +{ + "builders": [ + { + "type":"test", + } + ] +}