From dda7a7c8d664c2f17852e4adb6f7c35178da036a Mon Sep 17 00:00:00 2001 From: A-R-Narke Date: Wed, 22 Apr 2026 16:18:06 +0530 Subject: [PATCH] feat: add no-header flag to csv export (#841) --- cli/contacts_export.go | 19 +++++++++---- cli/contacts_export_test.go | 55 +++++++++++++++++++++++++++++++++++-- docs/docs/Features/CLI.md | 9 +++++- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/cli/contacts_export.go b/cli/contacts_export.go index 321e88ab382f31e869c1ef8e24ad69ca089b83b7..bfc34901453b22d41964077805f5d70083c1dc7b 100644 --- a/cli/contacts_export.go +++ b/cli/contacts_export.go @@ -17,6 +17,7 @@ func RunContactsExport(args []string) error { fs := flag.NewFlagSet("contacts export", flag.ExitOnError) format := fs.String("f", "json", "output format: json or csv") output := fs.String("o", "", "output file path (default: stdout)") + noHeader := fs.Bool("no-header", false, "omit CSV header row") help := fs.Bool("h", false, "show help") if err := fs.Parse(args); err != nil { @@ -35,6 +36,7 @@ func RunContactsExport(args []string) error { fmt.Println(" matcha contacts export # JSON to stdout") fmt.Println(" matcha contacts export -f csv # CSV to stdout") fmt.Println(" matcha contacts export -o out.json # JSON to file") + fmt.Println(" matcha contacts export -f csv --no-header # CSV without headers") return nil } @@ -43,10 +45,10 @@ func RunContactsExport(args []string) error { return fmt.Errorf("invalid format '%s': must be 'json' or 'csv'", *format) } - return runExportContacts(formatStr, *output) + return runExportContacts(formatStr, *output, *noHeader) } -func runExportContacts(format, outputPath string) error { +func runExportContacts(format, outputPath string, noHeader bool) error { var contacts []config.Contact var err error @@ -82,12 +84,15 @@ func runExportContacts(format, outputPath string) error { var outputData []byte if format == "json" { + if noHeader { + return fmt.Errorf("the --no-header flag is only valid with CSV format") + } outputData, err = exportToJSON(contacts) if err != nil { return fmt.Errorf("failed to export to JSON: %w", err) } } else { - outputData, err = exportToCSV(contacts) + outputData, err = exportToCSV(contacts, noHeader) if err != nil { return fmt.Errorf("failed to export to CSV: %w", err) } @@ -115,12 +120,14 @@ func exportToJSON(contacts []config.Contact) ([]byte, error) { return json.MarshalIndent(contacts, "", " ") } -func exportToCSV(contacts []config.Contact) ([]byte, error) { +func exportToCSV(contacts []config.Contact, noHeader bool) ([]byte, error) { var buf strings.Builder writer := csv.NewWriter(&buf) - if err := writer.Write([]string{"name", "email", "last_used", "use_count"}); err != nil { - return nil, err + if !noHeader { + if err := writer.Write([]string{"name", "email", "last_used", "use_count"}); err != nil { + return nil, err + } } for _, c := range contacts { diff --git a/cli/contacts_export_test.go b/cli/contacts_export_test.go index 94e70442069dce6b72b7ffa127d9d477006c144b..74f96114dbb5fe51687d79f41e9d1dd9f6f212d1 100644 --- a/cli/contacts_export_test.go +++ b/cli/contacts_export_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "testing" "time" @@ -61,7 +62,7 @@ func TestExportToCSV(t *testing.T) { }, } - data, err := exportToCSV(contacts) + data, err := exportToCSV(contacts, false) if err != nil { t.Fatalf("exportToCSV failed: %v", err) } @@ -86,6 +87,54 @@ func TestExportToCSV(t *testing.T) { } } +func TestExportToCSVNoHeader(t *testing.T) { + contacts := []config.Contact{ + { + Name: "John Doe", + Email: "john@example.com", + LastUsed: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC), + UseCount: 5, + }, + { + Name: "Jane Smith", + Email: "jane@test.com", + LastUsed: time.Date(2024, 2, 20, 14, 0, 0, 0, time.UTC), + UseCount: 10, + }, + } + + data, err := exportToCSV(contacts, true) + if err != nil { + t.Fatalf("exportToCSV failed: %v", err) + } + + output := string(data) + expectedFields := "name,email,last_used,use_count" + + // Check that header is NOT present anywhere in the output + if strings.Contains(output, expectedFields) { + t.Errorf("expected no CSV header, but found '%s' in output", expectedFields) + } + + // Check that both contacts are present + if !contains(output, "john@example.com") { + t.Error("expected john@example.com in CSV output") + } + if !contains(output, "jane@test.com") { + t.Error("expected jane@test.com in CSV output") + } + + // Check that the first line is data, not header + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 2 { + t.Fatal("expected at least 2 lines in CSV output") + } + firstLine := lines[0] + if !strings.Contains(firstLine, "john@example.com") { + t.Errorf("expected first line to contain contact email, got '%s'", firstLine) + } +} + func TestEscapeCSV(t *testing.T) { tests := []struct { input string @@ -121,7 +170,7 @@ func TestExportToCSVWithSpecialChars(t *testing.T) { }, } - data, err := exportToCSV(contacts) + data, err := exportToCSV(contacts, false) if err != nil { t.Fatalf("exportToCSV failed: %v", err) } @@ -192,7 +241,7 @@ func TestExportCSVToFile(t *testing.T) { }, } - data, err := exportToCSV(contacts) + data, err := exportToCSV(contacts, false) if err != nil { t.Fatalf("exportToCSV failed: %v", err) } diff --git a/docs/docs/Features/CLI.md b/docs/docs/Features/CLI.md index d61e52f2156d6d05cba3301b01fc205375c3b51d..bfc294458508bfe691d36783a849bb9ce10588e8 100644 --- a/docs/docs/Features/CLI.md +++ b/docs/docs/Features/CLI.md @@ -144,6 +144,7 @@ matcha contacts export [flags] |------|-------------| | `-f` | Output format: `json` or `csv` (default: `json`) | | `-o` | Output file path. If omitted, prints to stdout | +| `--no-header` | Omit CSV header row (CSV format only) | | `-h` | Show help | ### Examples @@ -167,13 +168,19 @@ matcha contacts export -o ~/contacts.json matcha contacts export -f csv -o ~/contacts.csv ``` +**Export CSV without headers:** + +```bash +matcha contacts export -f csv --no-header +``` + If encryption is enabled, you will be prompted for your password before the contacts can be read. ### Output Format **JSON** exports an array of contact objects with `name`, `email`, `last_used`, and `use_count` fields. -**CSV** exports a header row (`name,email,last_used,use_count`) followed by one row per contact. +**CSV** exports a header row (`name,email,last_used,use_count`) followed by one row per contact. Use `--no-header` to omit the header row. ## matcha config