CSV transforms for Go CLIs

Turn messy CSVs into useful tools.

simplecsv gives small command-line programs a clear path from raw rows to filtered, reshaped, joined, and summarized output. No database. No query language. Just Go.

stdlib only immutable operations all cells are strings
$ go run . people.csv

// read once
people, _ := simplecsv.ReadCsvFileE(path)

// field-first transforms
active, _ := people.Where("status", "active")
summary, _ := active.GroupBy(...)

// write a useful result
summary.WriteCsvFileE("report.csv")

ok 3,842 rows -> 17 groups
01 Read files or streams
02 Transform fields and rows
03 Segment filter and sort
04 Combine join and concat
05 Report group and write

Why simplecsv

Small surface. Serious invariants.

CSV scripts tend to start small. simplecsv keeps them small while taking care of the row-index mistakes and schema surprises that make them brittle.

01

Field names first

Say Where("status", "active"), not "find column 4 and remember the header."

02

Never mutate by accident

Transform methods return independent CSVs. Build a pipeline without hidden changes to earlier values.

03

All cells stay strings

Keep storage predictable. Parse numbers only where a filter, sort, or aggregate actually needs them.

04

More than read and write

Join files, deduplicate rows, reshape columns, group values, and build reports without pulling in a database.

05

Predictable failures

Unique headers and uniform row widths are enforced. Use *E variants when a useful error matters.

06

Ready for real exports

Read delimiters and streams, handle BOMs, limit untrusted input, and sanitize spreadsheet-bound output.

A useful first program

Readable enough to hand to your future self.

Most tools need the same sequence: load, keep the rows that matter, choose the output shape, and write it somewhere useful.

ReadCsvFileE Where FilterByField OnlyThisFields
package main

import (
    "log"
    "strconv"

    "github.com/osvik/simplecsv"
)

func main() {
    people, err := simplecsv.ReadCsvFileE("people.csv")
    if err != nil {
        log.Fatal(err)
    }

    active, ok := people.Where("status", "active")
    if !ok {
        log.Fatal("status field not found")
    }

    adults, ok := active.FilterByField("age", func(value string) bool {
        age, err := strconv.Atoi(value)
        return err == nil && age >= 18
    })
    if !ok {
        log.Fatal("age field not found")
    }

    result, ok := adults.OnlyThisFields([]string{"id", "name", "age"})
    if !ok {
        log.Fatal("could not select output fields")
    }

    if err := result.WriteCsvFileE("active-adults.csv"); err != nil {
        log.Fatal(err)
    }
}

Make the CSV utility you actually wanted.