diff --git a/pkg/output/serialized_output.go b/pkg/output/serialized_output.go index dfbf8405d5..2f9eae96c7 100644 --- a/pkg/output/serialized_output.go +++ b/pkg/output/serialized_output.go @@ -1,6 +1,7 @@ package output import ( + "bytes" "encoding/json" "github.com/spf13/cobra" @@ -12,7 +13,7 @@ import ( func SerializedOutput(cmd *cobra.Command, v any) error { switch GetFormat(cmd) { default: - out, err := json.Marshal(v) + out, err := marshalJSON(v) if err != nil { return err } @@ -26,3 +27,16 @@ func SerializedOutput(cmd *cobra.Command, v any) error { } return nil } + +// marshalJSON marshals v to JSON without HTML-escaping <, >, and & (which +// encoding/json does by default), keeping text like SQL statements readable. +func marshalJSON(v any) ([]byte, error) { + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(v); err != nil { + return nil, err + } + // Encode appends a single trailing newline; drop just it to match json.Marshal. + return bytes.TrimSuffix(buffer.Bytes(), []byte("\n")), nil +} diff --git a/pkg/output/table.go b/pkg/output/table.go index 07226087f1..8a43d3330c 100644 --- a/pkg/output/table.go +++ b/pkg/output/table.go @@ -1,7 +1,6 @@ package output import ( - "encoding/json" "fmt" "io" "reflect" @@ -126,7 +125,7 @@ func (t *Table) printCore(writer io.Writer, auto bool) error { switch t.format { default: - out, err := json.Marshal(v) + out, err := marshalJSON(v) if err != nil { return err } diff --git a/pkg/output/table_test.go b/pkg/output/table_test.go index 5ed13fcd7e..18fb134d33 100644 --- a/pkg/output/table_test.go +++ b/pkg/output/table_test.go @@ -128,6 +128,41 @@ func TestTable_NoAutoWrap(t *testing.T) { } } +func TestTable_NoHTMLEscape(t *testing.T) { + // <, >, and & must appear verbatim in -o json output, not HTML-escaped. + buf := new(bytes.Buffer) + cmd := &cobra.Command{} + cmd.Flags().String("output", JSON.String(), "") + cmd.SetOut(buf) + + table := NewTable(cmd) + table.Add(&out{ + Id: 1, + Name: "lkc-123456", + Description: "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true", + }) + + err := table.Print() + require.NoError(t, err) + + expected := strings.Join([]string{ + "{", + ` "is_current": false,`, + ` "id": 1,`, + ` "name": "lkc-123456",`, + ` "description": "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"`, + "}", + }, "\n") + "\n" + require.Equal(t, expected, buf.String()) +} + +func TestMarshalJSON_NoHTMLEscape(t *testing.T) { + // marshalJSON backs both the Table and SerializedOutput JSON paths. + out, err := marshalJSON(map[string]string{"statement": "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"}) + require.NoError(t, err) + require.Equal(t, `{"statement":"SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"}`, string(out)) +} + func TestTable_Filter(t *testing.T) { tests := map[string][]string{ Human.String(): { diff --git a/test/fixtures/output/api-key/7.golden b/test/fixtures/output/api-key/7.golden index e079df5e44..a891c83737 100644 --- a/test/fixtures/output/api-key/7.golden +++ b/test/fixtures/output/api-key/7.golden @@ -3,7 +3,7 @@ "key": "DEACTIVATEDUSERKEY", "description": "", "owner": "sa-6666", - "owner_email": "\u003cdeactivated user\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-bob", "created": "1999-02-24T00:00:00Z" @@ -30,7 +30,7 @@ "key": "MULTICLUSTERKEY3", "description": "works for two clusters and owned by service account", "owner": "sa-12345", - "owner_email": "\u003cservice account\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-abc", "created": "1999-02-24T00:00:00Z" @@ -66,7 +66,7 @@ "key": "SERVICEACCOUNTKEY1", "description": "", "owner": "sa-12345", - "owner_email": "\u003cservice account\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-bob", "created": "1999-02-24T00:00:00Z"