From cbfc16f354b7414e60901603e3bb9febd7ffce3e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 23 Jun 2026 08:37:26 +0900 Subject: [PATCH] add --jq-raw flag to print jq string results unquoted, like jq -r --- cli_test.go | 14 ++++++++++++++ event.go | 10 +++++++--- fetch.go | 8 ++++++-- filter.go | 8 ++++++-- jq.go | 29 ++++++++++++++++++++++------- req.go | 10 +++++++--- 6 files changed, 62 insertions(+), 17 deletions(-) diff --git a/cli_test.go b/cli_test.go index 1cb2fb9..7dd2ed6 100644 --- a/cli_test.go +++ b/cli_test.go @@ -229,3 +229,17 @@ func TestNaturalTimestamps(t *testing.T) { require.Equal(t, nostr.Timestamp(1526711839), evt.CreatedAt) require.Equal(t, "nn", evt.Content) } + +func TestEventJQ(t *testing.T) { + // default: string results come out JSON-quoted, like plain jq + quoted := call(t, "nak event --ts 1699485669 -c hello --jq .content") + require.Equal(t, `"hello"`, quoted) + + // --jq-raw: string results come out unquoted, like `jq -r` + raw := call(t, "nak event --ts 1699485669 -c hello --jq .content --jq-raw") + require.Equal(t, "hello", raw) + + // --jq-raw on a non-string result is still JSON-encoded, like `jq -r` + num := call(t, "nak event --ts 1699485669 -k 7 --jq .kind --jq-raw") + require.Equal(t, "7", num) +} diff --git a/event.go b/event.go index e5b4b97..b86bc3c 100644 --- a/event.go +++ b/event.go @@ -95,6 +95,11 @@ example: Usage: "filter returned events with jq expression", Category: CATEGORY_EXTRAS, }, + &cli.BoolFlag{ + Name: "jq-raw", + Usage: "print --jq string results without JSON quoting, like `jq -r`", + Category: CATEGORY_EXTRAS, + }, &cli.BoolFlag{ Name: "nevent", Usage: "print the nevent code (to stderr) after the event is published", @@ -187,7 +192,7 @@ example: return err } - jq, err := jqPrepare(c.String("jq")) + jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw")) if err != nil { return err } @@ -422,12 +427,11 @@ example: } stdout(result) } else { - v, matches, err := jq(evt) + out, matches, err := jq(evt) if err != nil { return fmt.Errorf("jq filter failed: %w", err) } if matches { - out, _ := json.MarshalToString(v) stdout(out) } } diff --git a/fetch.go b/fetch.go index b14c492..a112d18 100644 --- a/fetch.go +++ b/fetch.go @@ -28,10 +28,14 @@ var fetch = &cli.Command{ Name: "jq", Usage: "filter returned events with jq expression", }, + &cli.BoolFlag{ + Name: "jq-raw", + Usage: "print --jq string results without JSON quoting, like `jq -r`", + }, ), ArgsUsage: "[nip05_or_nip19_code]", Action: func(ctx context.Context, c *cli.Command) error { - jq, err := jqPrepare(c.String("jq")) + jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw")) if err != nil { return err } @@ -131,7 +135,7 @@ var fetch = &cli.Command{ if !matches { continue } - out, _ = json.MarshalToString(v) + out = v } stdout(out) } diff --git a/filter.go b/filter.go index 85b5a37..4bbc14b 100644 --- a/filter.go +++ b/filter.go @@ -24,6 +24,10 @@ example: Name: "jq", Usage: "filter matching events with jq expression", }, + &cli.BoolFlag{ + Name: "jq-raw", + Usage: "print --jq string results without JSON quoting, like `jq -r`", + }, ), ArgsUsage: "[event_json] [base_filter_json]", Action: func(ctx context.Context, c *cli.Command) error { @@ -59,7 +63,7 @@ example: return err } - jq, err := jqPrepare(c.String("jq")) + jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw")) if err != nil { return err } @@ -104,7 +108,7 @@ example: if !matches { continue } - out, _ = json.MarshalToString(v) + out = v } stdout(out) } else { diff --git a/jq.go b/jq.go index 6816b69..cfb86e3 100644 --- a/jq.go +++ b/jq.go @@ -19,9 +19,11 @@ def time: .created_at | gmtime | strftime("%H:%M:%S"); def datetime: .created_at | gmtime | strftime("%Y-%m-%dT%H:%M:%SZ"); ` -type jqProcessor func(nostr.Event) (any, bool, error) +// jqProcessor runs the compiled jq expression against an event and returns the +// rendered output string. When matches is false the event should be skipped. +type jqProcessor func(nostr.Event) (out string, matches bool, err error) -func jqPrepare(expr string) (jqProcessor, error) { +func jqPrepare(expr string, raw bool) (jqProcessor, error) { if expr == "" { return nil, nil } @@ -36,30 +38,43 @@ func jqPrepare(expr string) (jqProcessor, error) { return nil, fmt.Errorf("failed to compile jq expression: %w", err) } - return func(evt nostr.Event) (any, bool, error) { + return func(evt nostr.Event) (string, bool, error) { input, err := toJQInput(evt) if err != nil { - return nil, false, err + return "", false, err } iter := code.Run(input) for { v, ok := iter.Next() if !ok { - return v, false, nil + return "", false, nil } if err, ok := v.(error); ok { - return v, false, err + return "", false, err } if jqTruthy(v) { - return v, true, nil + return jqStringify(v, raw), true, nil } } }, nil } +// jqStringify renders a jq result value. In raw mode string results are printed +// without JSON quoting (like `jq -r`); non-string results are always +// JSON-encoded. +func jqStringify(v any, raw bool) string { + if raw { + if s, ok := v.(string); ok { + return s + } + } + out, _ := json.MarshalToString(v) + return out +} + func toJQInput(v any) (any, error) { data, err := json.Marshal(v) if err != nil { diff --git a/req.go b/req.go index cb799bf..98e37bb 100644 --- a/req.go +++ b/req.go @@ -46,6 +46,10 @@ example: Name: "jq", Usage: "filter returned events with jq expression", }, + &cli.BoolFlag{ + Name: "jq-raw", + Usage: "print --jq string results without JSON quoting, like `jq -r`", + }, &cli.BoolFlag{ Name: "no-verify", Usage: "skip event signature verification from relays", @@ -124,7 +128,7 @@ example: return fmt.Errorf("relay URLs are incompatible with --bare or --spell") } - jq, err := jqPrepare(c.String("jq")) + jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw")) if err != nil { return err } @@ -437,7 +441,7 @@ readevents: if !matches { continue } - out, _ = json.MarshalToString(v) + out = v } stdout(out) @@ -641,7 +645,7 @@ func (p PrintingQuerierPublisher) Publish(ctx context.Context, evt nostr.Event) if !matches { return nil } - out, _ = json.MarshalToString(v) + out = v } stdout(out) return nil