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 2b0126b..8467a67 100644 --- a/table_test.go +++ b/table_test.go @@ -366,6 +366,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 34149d2..9420085 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 { @@ -112,7 +113,7 @@ func (t *Table) flush(info []columnInfo, rows [][]cell, annotations []annotation } 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 } @@ -141,7 +142,7 @@ func (t *Table) flush(info []columnInfo, rows [][]cell, annotations []annotation } // 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), " ") @@ -174,6 +175,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) @@ -182,9 +191,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 } @@ -194,7 +201,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, " ") } } @@ -203,6 +210,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) annotationsFor(start, end int, includeStart bool) []annotation { var annotations []annotation @@ -236,6 +257,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