From e6b077af2d96e379ae38a864af94efbc5b53f1e0 Mon Sep 17 00:00:00 2001 From: "Mason J. Katz" Date: Fri, 21 Aug 2026 18:09:59 -0700 Subject: [PATCH 1/2] fix: omit fields tagged with table dash Fixes #3 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 ++ table_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ text.go | 39 +++++++++++++++++++++++++++++++++------ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a0a71c4..92dfb20 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ type host struct { } ``` +These tags affect text table output only. JSON and YAML output encode the original structs. + ### Annotations Insert comments or context between rows: diff --git a/table_test.go b/table_test.go index 0568bf0..90923bb 100644 --- a/table_test.go +++ b/table_test.go @@ -350,6 +350,55 @@ func TestOmitEmptyColumns(t *testing.T) { } } +func TestSkipField(t *testing.T) { + type record struct { + Name string + Secret string `table:"-"` + } + + var buf bytes.Buffer + + tbl := New(WithWriter(&buf)) + tbl.Write(record{Name: "text-visible", Secret: "hidden"}) + _ = tbl.Flush() + + output := buf.String() + if strings.Contains(output, "SECRET") || strings.Contains(output, "hidden") { + t.Errorf("expected skipped field to be omitted, got: %q", output) + } +} + +func TestSkipFieldDoesNotAffectStructuredOutput(t *testing.T) { + type record struct { + Name string + Secret string `table:"-"` + } + + t.Run("JSON", func(t *testing.T) { + var buf bytes.Buffer + + tbl := NewJSON(WithWriter(&buf)) + tbl.Write(record{Name: "json-visible", Secret: "included"}) + _ = tbl.Flush() + + if output := buf.String(); !strings.Contains(output, `"Secret": "included"`) { + t.Errorf("expected skipped field in JSON output, got: %q", output) + } + }) + + t.Run("YAML", func(t *testing.T) { + var buf bytes.Buffer + + tbl := NewYAML(WithWriter(&buf)) + tbl.Write(record{Name: "yaml-visible", Secret: "included"}) + _ = tbl.Flush() + + if output := buf.String(); !strings.Contains(output, "secret: included") { + t.Errorf("expected skipped field in YAML output, got: %q", output) + } + }) +} + func TestWrapperInterface(t *testing.T) { type status string diff --git a/text.go b/text.go index d6e65a1..2025941 100644 --- a/text.go +++ b/text.go @@ -19,6 +19,7 @@ type columnInfo struct { Width int OmitEmpty bool IsZero bool + Skip bool } type wrapper interface { @@ -113,7 +114,7 @@ func (t *Table) flush(info []columnInfo, rows [][]cell) { } for j := range rows[i] { - if info[j].IsZero { // skip empty columns TODO: make this configurable + if !info[j].visible() { // skip empty columns TODO: make this configurable continue } @@ -142,7 +143,7 @@ func (t *Table) flush(info []columnInfo, rows [][]cell) { } // Skip padding for the last column - if j == len(rows[i])-1 { + if j == lastVisibleColumn(info) { fmt.Fprint(t.writer, sgr.Wrap(rowColor, text)) } else { fmt.Fprint(t.writer, sgr.Wrap(rowColor, text, padding), " ") @@ -164,6 +165,14 @@ func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { } } + for j := range info { + if info[j].OmitEmpty && isColumnZero(j, rows) { + info[j].IsZero = true + } + } + + lastColumn := lastVisibleColumn(info) + for i := range numLines { if i > 0 { fmt.Fprintln(t.writer) @@ -172,9 +181,7 @@ func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { for j := range info { // header var label string - if info[j].OmitEmpty && isColumnZero(j, rows) { - info[j].IsZero = true - + if !info[j].visible() { continue } @@ -184,7 +191,7 @@ func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { fmt.Fprint(t.writer, sgr.Wrapf(t.colors.Header, "%-*s", info[j].Width, label)) - if j != len(info)-1 { + if j != lastColumn { fmt.Fprint(t.writer, " ") } } @@ -193,6 +200,20 @@ func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { fmt.Fprintln(t.writer) } +func lastVisibleColumn(info []columnInfo) int { + for i := len(info) - 1; i >= 0; i-- { + if info[i].visible() { + return i + } + } + + return -1 +} + +func (c columnInfo) visible() bool { + return !c.IsZero && !c.Skip +} + func (t *Table) processHeader(header reflect.Type) []columnInfo { numFields := header.NumField() @@ -211,6 +232,12 @@ func (t *Table) processHeader(header reflect.Type) []columnInfo { // Parse tag: "LABEL,omitempty" -> label="LABEL", omitEmpty=true label, options, _ := strings.Cut(tag, ",") + if label == "-" { + columns[i].Skip = true + + continue + } + if label != "" { labels := strings.Split(label, "\n") if labels[0] == "" { // use the default label if empty From 472a6c5240ce9bd6c7fffadec2458c8cc8dd0ff9 Mon Sep 17 00:00:00 2001 From: "Mason J. Katz" Date: Fri, 21 Aug 2026 18:24:57 -0700 Subject: [PATCH 2/2] ci: pin shared workflows to v1.3.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 99db0d5..3e8d465 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ on: jobs: lint: - uses: endobit/ci/.github/workflows/go-lint.yaml@main + uses: endobit/ci/.github/workflows/go-lint.yaml@v1.3.1 test: - uses: endobit/ci/.github/workflows/go-test.yaml@main + uses: endobit/ci/.github/workflows/go-test.yaml@v1.3.1 secrets: inherit