Skip to content

Commit 348e77b

Browse files
thobitfield
andauthored
Change JQ to process newline-delimited JSON (#227)
* Change JQ to process newline-delimited JSON https://jsonlines.org Example use case: Analyzes logs to count and display frequency of different log levels from newline-delimited JSON input. ``` cat log.json | ./goscript.sh -c 'script.Stdin().JQ(".level").Freq().Stdout()' 3 "INFO" 2 "WARN" 1 "ERROR" ``` * tweaks * remove extra failure outputs * add extra ignored test input --------- Co-authored-by: John Arundel <[email protected]>
1 parent 5b2f8f4 commit 348e77b

File tree

2 files changed

+82
-27
lines changed

2 files changed

+82
-27
lines changed

script.go

+34-22
Original file line numberDiff line numberDiff line change
@@ -712,38 +712,50 @@ func (p *Pipe) Join() *Pipe {
712712
})
713713
}
714714

715-
// JQ executes query on the pipe's contents (presumed to be JSON), producing
716-
// the result. An invalid query will set the appropriate error on the pipe.
715+
// JQ executes query on the pipe's contents (presumed to be valid JSON or
716+
// [JSONLines] data), applying the query to each newline-delimited input value
717+
// and producing results until the first error is encountered. An invalid query
718+
// or value will set the appropriate error on the pipe.
717719
//
718720
// The exact dialect of JQ supported is that provided by
719721
// [github.com/itchyny/gojq], whose documentation explains the differences
720722
// between it and standard JQ.
723+
//
724+
// [JSONLines]: https://jsonlines.org/
721725
func (p *Pipe) JQ(query string) *Pipe {
726+
parsedQuery, err := gojq.Parse(query)
727+
if err != nil {
728+
return p.WithError(err)
729+
}
730+
code, err := gojq.Compile(parsedQuery)
731+
if err != nil {
732+
return p.WithError(err)
733+
}
722734
return p.Filter(func(r io.Reader, w io.Writer) error {
723-
q, err := gojq.Parse(query)
724-
if err != nil {
725-
return err
726-
}
727-
var input interface{}
728-
err = json.NewDecoder(r).Decode(&input)
729-
if err != nil {
730-
return err
731-
}
732-
iter := q.Run(input)
733-
for {
734-
v, ok := iter.Next()
735-
if !ok {
736-
return nil
737-
}
738-
if err, ok := v.(error); ok {
739-
return err
740-
}
741-
result, err := gojq.Marshal(v)
735+
dec := json.NewDecoder(r)
736+
for dec.More() {
737+
var input any
738+
err := dec.Decode(&input)
742739
if err != nil {
743740
return err
744741
}
745-
fmt.Fprintln(w, string(result))
742+
iter := code.Run(input)
743+
for {
744+
v, ok := iter.Next()
745+
if !ok {
746+
break
747+
}
748+
if err, ok := v.(error); ok {
749+
return err
750+
}
751+
result, err := gojq.Marshal(v)
752+
if err != nil {
753+
return err
754+
}
755+
fmt.Fprintln(w, string(result))
756+
}
746757
}
758+
return nil
747759
})
748760
}
749761

script_test.go

+48-5
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,6 @@ func TestJQWithDotQueryPrettyPrintsInput(t *testing.T) {
743743
t.Fatal(err)
744744
}
745745
if want != got {
746-
t.Error(want, got)
747746
t.Error(cmp.Diff(want, got))
748747
}
749748
}
@@ -757,7 +756,6 @@ func TestJQWithFieldQueryProducesSelectedField(t *testing.T) {
757756
t.Fatal(err)
758757
}
759758
if want != got {
760-
t.Error(want, got)
761759
t.Error(cmp.Diff(want, got))
762760
}
763761
}
@@ -771,7 +769,6 @@ func TestJQWithArrayQueryProducesRequiredArray(t *testing.T) {
771769
t.Fatal(err)
772770
}
773771
if want != got {
774-
t.Error(want, got)
775772
t.Error(cmp.Diff(want, got))
776773
}
777774
}
@@ -785,7 +782,6 @@ func TestJQWithArrayInputAndElementQueryProducesSelectedElement(t *testing.T) {
785782
t.Fatal(err)
786783
}
787784
if want != got {
788-
t.Error(want, got)
789785
t.Error(cmp.Diff(want, got))
790786
}
791787
}
@@ -799,7 +795,32 @@ func TestJQHandlesGithubJSONWithRealWorldExampleQuery(t *testing.T) {
799795
t.Fatal(err)
800796
}
801797
if want != got {
802-
t.Error(want, got)
798+
t.Error(cmp.Diff(want, got))
799+
}
800+
}
801+
802+
func TestJQCorrectlyQueriesMultilineInputFields(t *testing.T) {
803+
t.Parallel()
804+
input := `{"a":1}` + "\n" + `{"a":2}`
805+
want := "1\n2\n"
806+
got, err := script.Echo(input).JQ(".a").String()
807+
if err != nil {
808+
t.Fatal(err)
809+
}
810+
if want != got {
811+
t.Error(cmp.Diff(want, got))
812+
}
813+
}
814+
815+
func TestJQCorrectlyQueriesMultilineInputArrays(t *testing.T) {
816+
t.Parallel()
817+
input := `[1, 2, 3]` + "\n" + `[4, 5, 6]`
818+
want := "1\n4\n"
819+
got, err := script.Echo(input).JQ(".[0]").String()
820+
if err != nil {
821+
t.Fatal(err)
822+
}
823+
if want != got {
803824
t.Error(cmp.Diff(want, got))
804825
}
805826
}
@@ -813,6 +834,28 @@ func TestJQErrorsWithInvalidQuery(t *testing.T) {
813834
}
814835
}
815836

837+
func TestJQErrorsWithInvalidInput(t *testing.T) {
838+
t.Parallel()
839+
input := "invalid JSON value"
840+
_, err := script.Echo(input).JQ(".").String()
841+
if err == nil {
842+
t.Error("want error from invalid JSON input, got nil")
843+
}
844+
}
845+
846+
func TestJQProducesValidResultsUntilFirstError(t *testing.T) {
847+
t.Parallel()
848+
input := "[1]\ninvalid JSON value\n[2]"
849+
want := "1\n"
850+
got, err := script.Echo(input).JQ(".[0]").String()
851+
if err == nil {
852+
t.Error("want error from invalid JSON input, got nil")
853+
}
854+
if want != got {
855+
t.Error(cmp.Diff(want, got))
856+
}
857+
}
858+
816859
func TestLastDropsAllButLastNLinesOfInput(t *testing.T) {
817860
t.Parallel()
818861
input := "a\nb\nc\n"

0 commit comments

Comments
 (0)