From d627bb917eb6f2564985234ded10695edfdfd5ff Mon Sep 17 00:00:00 2001 From: James Haggerty Date: Wed, 9 Aug 2017 17:15:23 +1000 Subject: [PATCH] Add line number to JSON parse error --- jp.go | 63 +++++++++++++++++++++++++++++++++++++++--- test/cases/search.bats | 14 ++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/jp.go b/jp.go index 9af9124..c68fe1a 100644 --- a/jp.go +++ b/jp.go @@ -3,8 +3,10 @@ package main import ( "encoding/json" "fmt" + "io" "io/ioutil" "os" + "sort" "github.com/jmespath/jp/Godeps/_workspace/src/github.com/codegangsta/cli" "github.com/jmespath/jp/Godeps/_workspace/src/github.com/jmespath/go-jmespath" @@ -83,19 +85,28 @@ func runMain(c *cli.Context) int { return 0 } var input interface{} - var jsonParser *json.Decoder + var inputStream io.Reader if c.String("filename") != "" { f, err := os.Open(c.String("filename")) if err != nil { return errMsg("Error opening input file: %s", err) } - jsonParser = json.NewDecoder(f) + inputStream = f } else { - jsonParser = json.NewDecoder(os.Stdin) + inputStream = os.Stdin } + newlineNumberReader := NewLineNumberReader(inputStream) + jsonParser := json.NewDecoder(newlineNumberReader) if err := jsonParser.Decode(&input); err != nil { - errMsg("Error parsing input json: %s\n", err) + syntaxError, ok := err.(*json.SyntaxError) + if ok && syntaxError.Offset == int64(int(syntaxError.Offset)) { + line, char := newlineNumberReader.ConvertOffset(int(syntaxError.Offset)) + errMsg("Error parsing input json: %s (line: %d, char: %d)\n", + syntaxError, line, char) + } else { + errMsg("Error parsing input json: %s", err) + } return 2 } result, err := jmespath.Search(expression, input) @@ -121,3 +132,47 @@ func runMain(c *cli.Context) int { os.Stdout.WriteString("\n") return 0 } + +type LineNumberReader struct { + actualReader io.Reader + newlinePositions []int + bytesRead int +} + +func NewLineNumberReader(actualReader io.Reader) *LineNumberReader { + return &LineNumberReader{ + actualReader: actualReader, + } +} + +func (lnr *LineNumberReader) Read(p []byte) (n int, err error) { + n, err = lnr.actualReader.Read(p) + + if err != nil || n == 0 { + return + } + + for i, v := range p { + if i >= n { + return + } + + if v == '\n' { + // add 1 so we record the position of the first character, not the '\n' + lnr.newlinePositions = append(lnr.newlinePositions, lnr.bytesRead+i+1) + } + } + + lnr.bytesRead = lnr.bytesRead + n + return +} + +func (lnr *LineNumberReader) ConvertOffset(offset int) (linePos int, charPos int) { + index := sort.SearchInts(lnr.newlinePositions, offset) + // Humans are 1 indexed... + if index == 0 { + return 1, offset + } else { + return index + 1, offset - lnr.newlinePositions[index-1] + } +} diff --git a/test/cases/search.bats b/test/cases/search.bats index 5285185..292e6bf 100644 --- a/test/cases/search.bats +++ b/test/cases/search.bats @@ -69,3 +69,17 @@ ASTField { [ "$status" -eq 0 ] [ "$output" == "12345" ] } + +@test "Report error in JSON with no newlines" { + echo -n '{"foo": bar}' > "$BATS_TMPDIR/input.json" + run ./jp -f "$BATS_TMPDIR/input.json" '@' + [ "$status" -eq 2 ] + [ "$output" == "Error parsing input json: invalid character 'b' looking for beginning of value (line: 1, char: 9)" ] +} + +@test "Report error in JSON with newlines" { + echo -en '{"foo": \nbar}' > "$BATS_TMPDIR/input.json" + run ./jp -f "$BATS_TMPDIR/input.json" '@' + [ "$status" -eq 2 ] + [ "$output" == "Error parsing input json: invalid character 'b' looking for beginning of value (line: 2, char: 1)" ] +}