From 2a5c8e805286162e961cd19fb4e646bea5a2a8cb Mon Sep 17 00:00:00 2001 From: Trevor Stone Date: Tue, 20 Aug 2024 23:00:43 -0600 Subject: [PATCH] Option to omit CSV or TSV header line --csv-omit-header and --tsv-omit-header will only print records, skipping the first line with field names. This is useful when output will be passed to another command like `uniq` without needing to pipe through `tail +2` first. When using these options, `adifmt select -fields` is a good idea to ensure fields match expectations. --- adif/csv.go | 7 +++++-- adif/csv_test.go | 35 +++++++++++++++++++++++++++++++++++ adif/tsv.go | 15 +++++++++------ adif/tsv_test.go | 35 +++++++++++++++++++++++++++++++++++ adifmt/formats.go | 4 +++- 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/adif/csv.go b/adif/csv.go index e2c0a8c..2b40b84 100644 --- a/adif/csv.go +++ b/adif/csv.go @@ -29,6 +29,7 @@ type CSVIO struct { LazyQuotes bool RequireFullRecord bool TrimLeadingSpace bool + OmitHeader bool } func NewCSVIO() *CSVIO { @@ -114,8 +115,10 @@ func (o *CSVIO) Write(l *Logfile, out io.Writer) error { c.Comma = o.Comma c.UseCRLF = o.CRLF // CSV header row - if err := c.Write(order); err != nil { - return fmt.Errorf("writing CSV header to %s: %w", l, err) + if !o.OmitHeader { + if err := c.Write(order); err != nil { + return fmt.Errorf("writing CSV header to %s: %w", l, err) + } } row := make([]string, len(order)) for i, r := range l.Records { diff --git a/adif/csv_test.go b/adif/csv_test.go index 7fbcee9..80f09ba 100644 --- a/adif/csv_test.go +++ b/adif/csv_test.go @@ -165,3 +165,38 @@ Switzerland",",comma notes,,," } } } + +func TestCSVOmitHeader(t *testing.T) { + l := NewLogfile() + l.Comment = "CSV ignores comments" + l.AddRecord(NewRecord( + Field{Name: "QSO_DATE", Value: "19901031", Type: TypeDate}, + Field{Name: "TIME_ON", Value: "1234", Type: TypeTime}, + Field{Name: "BAND", Value: "40M"}, + Field{Name: "CALLSIGN", Value: "W1AW"}, + Field{Name: "NAME", Value: "Hiram Percy Maxim", Type: TypeString}, + Field{Name: "FREQ", Value: "7.054"}, + )).AddRecord(NewRecord( + Field{Name: "QSO_DATE", Value: "20221224"}, + Field{Name: "TIME_ON", Value: "095846"}, + Field{Name: "BAND", Value: "1.25cm", Type: TypeEnumeration}, + Field{Name: "CALLSIGN", Value: "N0P", Type: TypeString}, + Field{Name: "NAME", Value: "Santa Claus"}, + Field{Name: "NOTES_INTL", Value: `Þhrough the /❄\ bringing 🎁 \to the child\re\n`}, + )) + l.Records[1].SetComment("Record comment") + want := `19901031,1234,40M,W1AW,Hiram Percy Maxim,7.054, +20221224,095846,1.25cm,N0P,Santa Claus,,Þhrough the /❄\ bringing 🎁 \to the child\re\n +` + csv := NewCSVIO() + csv.OmitHeader = true + out := &strings.Builder{} + if err := csv.Write(l, out); err != nil { + t.Errorf("Write(%v) got error %v", l, err) + } else { + got := out.String() + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Write(%v) had diff with expected:\n%s", l, diff) + } + } +} diff --git a/adif/tsv.go b/adif/tsv.go index 78aa2d8..b206d67 100644 --- a/adif/tsv.go +++ b/adif/tsv.go @@ -26,6 +26,7 @@ type TSVIO struct { CRLF bool EscapeSpecial bool IgnoreEmptyHeaders bool + OmitHeader bool } func NewTSVIO() *TSVIO { return &TSVIO{} } @@ -127,12 +128,14 @@ func (o *TSVIO) Write(l *Logfile, w io.Writer) error { } return nil } - for i, h := range order { - if _, err := out.WriteString(o.escape(h)); err != nil { - return fmt.Errorf("writing TSV header: %w", err) - } - if err := writeDelim(i); err != nil { - return fmt.Errorf("writing TSV header: %w", err) + if !o.OmitHeader { + for i, h := range order { + if _, err := out.WriteString(o.escape(h)); err != nil { + return fmt.Errorf("writing TSV header: %w", err) + } + if err := writeDelim(i); err != nil { + return fmt.Errorf("writing TSV header: %w", err) + } } } for _, r := range l.Records { diff --git a/adif/tsv_test.go b/adif/tsv_test.go index fd57a58..c9e7ed3 100644 --- a/adif/tsv_test.go +++ b/adif/tsv_test.go @@ -183,3 +183,38 @@ func TestTSVEscapeSpecialCharacters(t *testing.T) { } } } + +func TestTSVOmitHeader(t *testing.T) { + l := NewLogfile() + l.Comment = "TSV ignores comments" + l.FieldOrder = []string{"QSO_DATE", "TIME_ON", "BAND", "CALL"} + l.AddRecord(NewRecord( + Field{Name: "TIME_ON", Value: "1234", Type: TypeTime}, + Field{Name: "QSO_DATE", Value: "19901031", Type: TypeDate}, + Field{Name: "NAME", Value: "Hiram Percy Maxim", Type: TypeString}, + Field{Name: "BAND", Value: "40M"}, + Field{Name: "FREQ", Value: "7.054"}, + Field{Name: "CALL", Value: "W1AW"}, + )).AddRecord(NewRecord( + Field{Name: "notes_intl", Value: "Þhrough the /❄\\ bringing 🎁 \\to the child\\re\\n"}, + Field{Name: "qso_date", Value: "20221224"}, + Field{Name: "call", Value: "N0P", Type: TypeString}, + Field{Name: "time_on", Value: "095846"}, + Field{Name: "band", Value: "1.25cm", Type: TypeEnumeration}, + Field{Name: "name", Value: "Santa \"St. Nick\" Claus"}, + )) + l.Records[1].SetComment("Record comment") + want := "19901031\t1234\t40M\tW1AW\tHiram Percy Maxim\t7.054\t\n" + + "20221224\t095846\t1.25cm\tN0P\tSanta \"St. Nick\" Claus\t\tÞhrough the /❄\\ bringing 🎁 \\to the child\\re\\n\n" + tsv := NewTSVIO() + tsv.OmitHeader = true + out := &strings.Builder{} + if err := tsv.Write(l, out); err != nil { + t.Errorf("Write(%v) got error %v", l, err) + } else { + got := out.String() + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Write(%v) had diff with expected:\n%s", l, diff) + } + } +} diff --git a/adifmt/formats.go b/adifmt/formats.go index a2dd9c2..b4be916 100644 --- a/adifmt/formats.go +++ b/adifmt/formats.go @@ -121,6 +121,7 @@ func (c csvConfig) AddFlags(fs *flag.FlagSet) { fs.BoolVar(&c.io.RequireFullRecord, "csv-require-all-fields", false, "CSV files: error if fewer fields in a record than in header") fs.BoolVar(&c.io.TrimLeadingSpace, "csv-trim-space", false, "CSV files: ignore leading space in fields") fs.BoolVar(&c.io.CRLF, "csv-crlf", false, "CSV files: output MS Windows line endings") + fs.BoolVar(&c.io.OmitHeader, "csv-omit-header", false, "CSV files: don't output the header line") } type jsonConfig struct{ io *adif.JSONIO } @@ -145,5 +146,6 @@ func (c tsvConfig) IO() adif.ReadWriter { return c.io } func (c tsvConfig) AddFlags(fs *flag.FlagSet) { fs.BoolVar(&c.io.CRLF, "tsv-crlf", false, "TSV files: output MS Windows line endings") fs.BoolVar(&c.io.EscapeSpecial, "tsv-escape-special", false, "TSV files: accept and produce \\t \\r \\n and \\\\ escapes in fields") - fs.BoolVar(&c.io.IgnoreEmptyHeaders, "tsv-ignore-empty-headers", false, "TSV files: do not return error if a TSV file has an empty header field") + fs.BoolVar(&c.io.IgnoreEmptyHeaders, "tsv-ignore-empty-headers", false, "TSV files: don't return error if a TSV file has an empty header field") + fs.BoolVar(&c.io.OmitHeader, "tsv-omit-header", false, "TSV files: don't output the header line") }