@@ -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 {
@@ -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)
}
@@ -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