Skip to content

Commit 53cb451

Browse files
committed
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" ```
1 parent 5b2f8f4 commit 53cb451

File tree

2 files changed

+101
-16
lines changed

2 files changed

+101
-16
lines changed

script.go

+25-16
Original file line numberDiff line numberDiff line change
@@ -712,9 +712,10 @@ 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.
717-
//
715+
// JQ executes query on the pipe's contents (presumed to be newline-delimited
716+
// JSON), applying the query to each input value and producing the results. An
717+
// invalid query or value will set the appropriate error on the pipe.
718+
718719
// The exact dialect of JQ supported is that provided by
719720
// [github.com/itchyny/gojq], whose documentation explains the differences
720721
// between it and standard JQ.
@@ -724,26 +725,34 @@ func (p *Pipe) JQ(query string) *Pipe {
724725
if err != nil {
725726
return err
726727
}
727-
var input interface{}
728-
err = json.NewDecoder(r).Decode(&input)
728+
c, err := gojq.Compile(q)
729729
if err != nil {
730730
return err
731731
}
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)
732+
dec := json.NewDecoder(r)
733+
for dec.More() {
734+
var input interface{}
735+
err := dec.Decode(&input)
742736
if err != nil {
743737
return err
744738
}
745-
fmt.Fprintln(w, string(result))
739+
iter := c.Run(input)
740+
for {
741+
v, ok := iter.Next()
742+
if !ok {
743+
break
744+
}
745+
if err, ok := v.(error); ok {
746+
return err
747+
}
748+
result, err := gojq.Marshal(v)
749+
if err != nil {
750+
return err
751+
}
752+
fmt.Fprintln(w, string(result))
753+
}
746754
}
755+
return nil
747756
})
748757
}
749758

script_test.go

+76
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,59 @@ func TestJQHandlesGithubJSONWithRealWorldExampleQuery(t *testing.T) {
804804
}
805805
}
806806

807+
func TestJQWithNewlineDelimitedInputAndFieldQueryProducesSelectedFields(t *testing.T) {
808+
t.Parallel()
809+
input := `{"timestamp": 1649264191, "iss_position": {"longitude": "52.8439", "latitude": "10.8107"}, "message": "success"}` + "\n"
810+
input += input
811+
want := `{"latitude":"10.8107","longitude":"52.8439"}` + "\n"
812+
want += want
813+
got, err := script.Echo(input).JQ(".iss_position").String()
814+
if err != nil {
815+
t.Fatal(err)
816+
}
817+
if want != got {
818+
t.Error(want, got)
819+
t.Error(cmp.Diff(want, got))
820+
}
821+
}
822+
823+
func TestJQWithNewlineDelimitedInputAndArrayInputAndElementQueryProducesSelectedElements(t *testing.T) {
824+
t.Parallel()
825+
input := `[1, 2, 3]` + "\n" + `[4, 5, 6]`
826+
want := "1\n4\n"
827+
got, err := script.Echo(input).JQ(".[0]").String()
828+
if err != nil {
829+
t.Fatal(err)
830+
}
831+
if want != got {
832+
t.Error(want, got)
833+
t.Error(cmp.Diff(want, got))
834+
}
835+
}
836+
837+
func TestJQWithNewlineDelimitedMixedAndPrettyPrintedInputValues(t *testing.T) {
838+
t.Parallel()
839+
input := `
840+
{
841+
"key1": "val1",
842+
"key2": "val2"
843+
}
844+
[
845+
0,
846+
1
847+
]
848+
`
849+
want := `{"key1":"val1","key2":"val2"}` + "\n" + "[0,1]" + "\n"
850+
got, err := script.Echo(input).JQ(".").String()
851+
if err != nil {
852+
t.Fatal(err)
853+
}
854+
if want != got {
855+
t.Error(want, got)
856+
t.Error(cmp.Diff(want, got))
857+
}
858+
}
859+
807860
func TestJQErrorsWithInvalidQuery(t *testing.T) {
808861
t.Parallel()
809862
input := `[1, 2, 3]`
@@ -813,6 +866,29 @@ func TestJQErrorsWithInvalidQuery(t *testing.T) {
813866
}
814867
}
815868

869+
func TestJQErrorsWithInvalidInput(t *testing.T) {
870+
t.Parallel()
871+
input := "invalid JSON value"
872+
_, err := script.Echo(input).JQ(".").String()
873+
if err == nil {
874+
t.Error("want error from invalid JSON input, got nil")
875+
}
876+
}
877+
878+
func TestJQWithNewlineDelimitedInputErrorsAfterFirstInvalidInput(t *testing.T) {
879+
t.Parallel()
880+
input := `[0]` + "\n" + `[1` + "\n" + `[2]` // missing `]` in second line
881+
want := "0\n"
882+
got, err := script.Echo(input).JQ(".[0]").String()
883+
if err == nil {
884+
t.Fatal("want error from invalid JSON, got nil")
885+
}
886+
if want != got {
887+
t.Error(want, got)
888+
t.Error(cmp.Diff(want, got))
889+
}
890+
}
891+
816892
func TestLastDropsAllButLastNLinesOfInput(t *testing.T) {
817893
t.Parallel()
818894
input := "a\nb\nc\n"

0 commit comments

Comments
 (0)