Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ CSV++ extends traditional CSV to support **arrays** and **structured fields** wi

## Requirements

- Go 1.24 or later
- Go 1.25 or later
- `GOEXPERIMENT=jsonv2` environment variable (required by `csvpputil` package, which uses `encoding/json/jsontext`)

## Installation

```bash
go get github.com/osamingo/go-csvpp
```

When building or testing packages that depend on `csvpputil`, set the experiment flag:

```bash
GOEXPERIMENT=jsonv2 go build ./...
GOEXPERIMENT=jsonv2 go test ./...
```

## Quick Start

### Reading CSV++ Data
Expand Down Expand Up @@ -234,29 +242,17 @@ writer.Flush() // Flush buffer
### Marshal/Unmarshal

```go
// Unmarshal CSV++ data into structs
// Unmarshal CSV++ data into structs (r is io.Reader)
var people []Person
err := csvpp.Unmarshal(reader, &people)
err := csvpp.Unmarshal(r, &people)

// Marshal structs to CSV++ data
err := csvpp.Marshal(writer, people)
// Marshal structs to CSV++ data (w is io.Writer)
err := csvpp.Marshal(w, people)
```

### Struct Tags

Use `csvpp` struct tags to map fields:

```go
type Record struct {
Name string `csvpp:"name"` // Simple field
Tags []string `csvpp:"tags[]"` // Array field
Location struct { // Structured field
Lat string
Lon string
} `csvpp:"geo(lat^lon)"`
Addresses []Address `csvpp:"addr[](street^city)"` // Array structured
}
```
See [Struct Mapping](#struct-mapping) above for tag syntax and usage.

## JSON/YAML Conversion (csvpputil)

Expand Down
14 changes: 11 additions & 3 deletions cmd/csvpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ A command-line tool for working with CSV++ files.
## Installation

```bash
go install github.com/osamingo/go-csvpp/cmd/csvpp@latest
GOEXPERIMENT=jsonv2 go install github.com/osamingo/go-csvpp/cmd/csvpp@latest
```

Or build from source:

```bash
go build -o csvpp ./cmd/csvpp
GOEXPERIMENT=jsonv2 go build -o csvpp ./cmd/csvpp
```

> **Note:** `GOEXPERIMENT=jsonv2` is required because this tool depends on `encoding/json/jsontext` (Go 1.25+).

## Commands

### validate
Expand Down Expand Up @@ -82,9 +84,15 @@ cat input.csvpp | csvpp view
| `↑` / `↓` | Navigate rows |
| `Space` | Toggle row selection |
| `y` / `c` | Copy header + selected rows to clipboard (CSV++ format) |
| `Esc` | Clear selection |
| `/` | Open filter input |
| `Enter` | Apply filter (in filter mode) |
| `Esc` | Cancel filter / Clear active filter / Clear selection |
| `q` / `Ctrl+C` | Quit |

**Filter syntax:**
- Type text to search all columns (e.g., `Alice`)
- Use `column:value` to search a specific column (e.g., `name:Alice`)

**Note:** When stdin is not a TTY (e.g., in a pipe), a plain text table is displayed instead of the interactive TUI.

## Examples
Expand Down
7 changes: 6 additions & 1 deletion csvpputil/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ if err := w.Close(); err != nil {
#### YAMLArrayWriter

```go
w := csvpputil.NewYAMLArrayWriter(os.Stdout, headers)
w := csvpputil.NewYAMLArrayWriter(os.Stdout, headers,
csvpputil.WithYAMLCapacity(1000), // optional: pre-allocate buffer
)

for _, record := range records {
if err := w.Write(record); err != nil {
Expand All @@ -55,6 +57,9 @@ if err := w.Close(); err != nil {
}
```

**Options:**
- `WithYAMLCapacity(n)` - Pre-allocates the internal buffer for `n` records, reducing memory allocations when the approximate record count is known.

**Note:** YAML output is buffered until `Close()` due to go-yaml library constraints.

### Convenience Functions
Expand Down
5 changes: 5 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ var (
ExtractTagName = extractTagName
CachedTypeInfo = cachedTypeInfo
)

// ReaderLine returns the current line number of the reader for testing.
func ReaderLine(r *Reader) int {
return r.line
}
1 change: 1 addition & 0 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (r *Reader) ReadAll() ([][]*Field, error) {

result := make([][]*Field, 0, len(records))
for _, record := range records {
r.line++
fields, err := r.parseRecord(record)
if err != nil {
return nil, err
Expand Down
17 changes: 17 additions & 0 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,23 @@ func TestReader_ComponentsMoreThanHeaders(t *testing.T) {
}
}

func TestReader_ReadAll_LineTracking(t *testing.T) {
t.Parallel()

input := "name,age\nAlice,30\nBob,25\nCharlie,35\n"
r := csvpp.NewReader(strings.NewReader(input))

_, err := r.ReadAll()
if err != nil {
t.Fatalf("Reader.ReadAll() error = %v", err)
}

// After reading header (line 1) + 3 data records, line should be 4.
if got := csvpp.ReaderLine(r); got != 4 {
t.Errorf("Reader.ReadAll() line = %d, want 4", got)
}
}

func TestReader_ArrayStructuredInComponents(t *testing.T) {
t.Parallel()

Expand Down
3 changes: 3 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ func formatComponentList(components []*ColumnHeader, delim rune) string {

// formatField converts a Field to a string.
func (w *Writer) formatField(header *ColumnHeader, field *Field) string {
if field == nil {
return ""
}
if header == nil {
return field.Value
}
Expand Down
29 changes: 29 additions & 0 deletions writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,35 @@ func TestWriter_NoHeaderForRecord(t *testing.T) {
}
}

func TestWriter_Write_NilField(t *testing.T) {
t.Parallel()

headers := []*csvpp.ColumnHeader{
{Name: "name", Kind: csvpp.SimpleField},
{Name: "age", Kind: csvpp.SimpleField},
}
record := []*csvpp.Field{
{Value: "Alice"},
nil, // nil field should not panic
}

var buf bytes.Buffer
w := csvpp.NewWriter(&buf)
w.SetHeaders(headers)

err := w.Write(record)
if err != nil {
t.Fatalf("Writer.Write() error = %v", err)
}

w.Flush()
want := "Alice,\n"
got := buf.String()
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Writer.Write() mismatch (-want +got):\n%s", diff)
}
}

func TestFormatComponentList(t *testing.T) {
t.Parallel()

Expand Down