From c930ea5376316ba18d82ead0ef66eda14406b5f2 Mon Sep 17 00:00:00 2001 From: David Norton Date: Wed, 8 Apr 2026 08:01:42 -0500 Subject: [PATCH] Support jsonpath, go-template, custom-columns, and name output formats These formats produce raw text rather than structured K8s resources, so they are routed through a line-prefixed formatter that prepends the context name to each output line. Rename formatLogsOutput to formatRawOutput to reflect its broader use. Closes #73 --- cmd/output.go | 17 ++++++++++++-- cmd/output_test.go | 57 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/cmd/output.go b/cmd/output.go index e687103..f30ddc3 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -18,6 +18,7 @@ const ( formatDefault outputFormat = "default" formatJSON outputFormat = "json" formatYAML outputFormat = "yaml" + formatRaw outputFormat = "raw" ) const ( @@ -83,6 +84,16 @@ func detectOutputFormat(args []string) outputFormat { if format == "yaml" { return formatYAML } + if format == "name" || + strings.HasPrefix(format, "jsonpath=") || + strings.HasPrefix(format, "jsonpath-as-json=") || + strings.HasPrefix(format, "jsonpath-file=") || + strings.HasPrefix(format, "go-template=") || + strings.HasPrefix(format, "go-template-file=") || + strings.HasPrefix(format, "custom-columns=") || + strings.HasPrefix(format, "custom-columns-file=") { + return formatRaw + } return formatDefault } @@ -119,12 +130,14 @@ func formatOutput(results []contextResult, format outputFormat, subcommand strin return formatJSONOutput(results, subcommand) case formatYAML: return formatYAMLOutput(results, subcommand) + case formatRaw: + return formatRawOutput(results) default: if subcommand == "version" { return formatVersionOutput(results) } if subcommand == "logs" || subcommand == "api-versions" { - return formatLogsOutput(results) + return formatRawOutput(results) } return formatDefaultOutput(results) } @@ -406,7 +419,7 @@ func formatVersionOutput(results []contextResult) error { return nil } -func formatLogsOutput(results []contextResult) error { +func formatRawOutput(results []contextResult) error { maxContextWidth := 0 for _, result := range results { if len(result.context) > maxContextWidth { diff --git a/cmd/output_test.go b/cmd/output_test.go index f50cf14..0044809 100644 --- a/cmd/output_test.go +++ b/cmd/output_test.go @@ -103,6 +103,51 @@ func TestDetectOutputFormat(t *testing.T) { args: []string{"pod", "-otable"}, expected: formatDefault, }, + { + name: "name format", + args: []string{"pods", "-o", "name"}, + expected: formatRaw, + }, + { + name: "jsonpath format", + args: []string{"pods", "-o", "jsonpath={.items[*].metadata.name}"}, + expected: formatRaw, + }, + { + name: "jsonpath-as-json format", + args: []string{"pods", "-o", "jsonpath-as-json={.items[*]}"}, + expected: formatRaw, + }, + { + name: "jsonpath-file format", + args: []string{"pods", "-o", "jsonpath-file=tmpl.txt"}, + expected: formatRaw, + }, + { + name: "go-template format", + args: []string{"pods", "-o", "go-template={{range .items}}{{.metadata.name}}{{end}}"}, + expected: formatRaw, + }, + { + name: "go-template-file format", + args: []string{"pods", "-o", "go-template-file=tmpl.txt"}, + expected: formatRaw, + }, + { + name: "custom-columns format", + args: []string{"pods", "-o", "custom-columns=NAME:.metadata.name"}, + expected: formatRaw, + }, + { + name: "custom-columns-file format", + args: []string{"pods", "-o", "custom-columns-file=cols.txt"}, + expected: formatRaw, + }, + { + name: "jsonpath via equals flag", + args: []string{"pods", "--output=jsonpath={.items[*].metadata.name}"}, + expected: formatRaw, + }, } for _, tt := range tests { @@ -236,7 +281,7 @@ func TestFormatDefaultOutputErrorsBeforeOutput(t *testing.T) { assert.Less(t, errIdx, normalIdx, "error should appear before normal output") } -func TestFormatLogsOutput(t *testing.T) { +func TestFormatRawOutput(t *testing.T) { tests := []struct { name string results []contextResult @@ -291,7 +336,7 @@ func TestFormatLogsOutput(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := captureStdout(func() { - err := formatLogsOutput(tt.results) + err := formatRawOutput(tt.results) require.NoError(t, err) }) assert.Equal(t, tt.expected, output) @@ -299,7 +344,7 @@ func TestFormatLogsOutput(t *testing.T) { } } -func TestFormatLogsOutputErrorsToStderr(t *testing.T) { +func TestFormatRawOutputErrorsToStderr(t *testing.T) { oldStderr := os.Stderr stderrR, stderrW, _ := os.Pipe() os.Stderr = stderrW @@ -326,7 +371,7 @@ func TestFormatLogsOutputErrorsToStderr(t *testing.T) { {context: "bad-ctx", output: "some error detail", err: fmt.Errorf("connection refused")}, } - err := formatLogsOutput(results) + err := formatRawOutput(results) stdoutW.Close() stderrW.Close() <-stdoutDone @@ -340,14 +385,14 @@ func TestFormatLogsOutputErrorsToStderr(t *testing.T) { assert.Contains(t, stderrBuf.String(), "connection refused") } -func TestFormatLogsOutputErrorsBeforeOutput(t *testing.T) { +func TestFormatRawOutputErrorsBeforeOutput(t *testing.T) { results := []contextResult{ {context: "ctx1", output: "log line one\nlog line two"}, {context: "ctx2", output: "error message", err: fmt.Errorf("connection failed")}, } combined := captureOutputCombined(func() { - formatLogsOutput(results) + formatRawOutput(results) }) errIdx := strings.Index(combined, "Error:")