Projects/Build a CSV Parser

Build a CSV Parser

Start with a parser that reads a single field and end with a library that turns messy CSV text into records, streams one record at a time, maps rows to headers, and writes records back out quoting exactly the fields that need it. Every lesson is one concrete spec with exact records, fields, output, and error positions: a quoted field keeps an embedded comma and newline, a doubled quote collapses to one, a lone carriage return inside quotes survives, an unterminated quote errors at its exact line and column, and write-then-parse round-trips.

27 lessonsSmall~20 min / lessonRFC 4180Finite-state machinesStreaming parsers
The project

What you'll build over the next 27 lessons

Over 27 lessons you build a working CSV library from scratch. It begins as a finite-state machine that walks the input one rune at a time: first splitting a plain comma-and-newline table into records and fields, then growing a quoting state machine so a field wrapped in double quotes can hold the delimiter, a newline, and a doubled-quote escape, with clear errors that name the line and column of an unterminated quote or stray text after a closing quote. From there you handle the real world: carriage-return and CRLF line endings, a lone carriage return preserved inside quotes, significant leading and trailing spaces, and a leading UTF-8 byte-order mark.

On top of that core you add a configurable dialect (a custom delimiter, optional trimming, comment lines), a streaming reader that yields one record at a time with optional strict field-count checking for ragged rows, and a header mode that maps each row to a name-to-value record. Then you build the other half of the library, a writer that emits records and quotes exactly the fields that need it, proven correct by round-trip equivalence, and finish with a capstone that streams a messy real-world file (quotes, an embedded newline, CRLF, a byte-order mark, a ragged row) into records and writes a normalized file back.

This is a teaching-grade CSV library built around RFC 4180 and the finite-state-machine design that real parsers use: it parses and writes conforming CSV exactly, handles the common messy variations, and reports malformed input with precise positions. It is honest about its choices, which it states as it goes and collects in the caveats: it works on decoded UTF-8 text, treats a blank line as one empty field, and resolves duplicate header names last-wins. What you finish with is the honest core that production CSV libraries extend with type inference, richer encodings, and larger dialect matrices.

build-a-csv-parser / lesson-01.md
Lesson 01 · A table of records and fields

One field, one record

A CSV file is a table of records, and each record is a list of fields. Today you build the smallest possible parser, one that reads a single unpunctuated chunk of text as a table with one record holding one field, so the public surface exists from day one and every later lesson thickens it.

The goal

Parse a chunk of text with no commas or newlines into a table of one record holding one field.

Start here - the target
TO DO
Scenario: Parsing the simplest possible input
Giventhe input "abc"
Whenit is parsed into records
Thenthe result is one record holding one field, so the whole table is [["abc"]]
Andparsing the empty input "" yields zero records, the empty table []
Background

Every CSV document is a table: an ordered list of records (the rows), and every record is an ordered list of fields (the cells). That two-level shape, records made of fields, is the whole data model, so the type your parser returns is a list of lists of strings. Start with the smallest input that has any content at all, a single run of characters with no commas and no line breaks. It is one record, and that record has exactly one field.

The one decision worth making now is what empty input means. A file with no bytes has no rows in it, so parsing "" returns zero records, an empty table, not a table containing one empty row. Keep that distinction in mind: absence of any record is different from a record that happens to hold an empty field, which is a case you will meet very soon. Return a plain list of records for now; the commas, the newlines, and the quoting all arrive one lesson at a time on top of this shape.

Make it work
// records are a slice of records; each record is a slice of string fields
func Parse(input string) [][]string {
if input == "" {
return [][]string{}
}
return [][]string{{input}}
}
CheckpointDONE
You have an importable parser that turns a chunk of text into a one-record, one-field table, and treats empty input as zero records. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

The library and CLI are complete for a well-behaved comma or simple-dialect CSV workflow with clear positioned error reporting, but parsing loads the whole input into memory and the csvtool CLI does not yet expose the library dialect options as flags.

Extend it next
  • Expose the dialect options (custom delimiter, comment character, trim, strict mode) as csvtool flags instead of always using the default comma dialect
  • Replace the whole-input in-memory parse with a truly incremental reader over a byte stream, so genuinely huge files use constant memory
  • Add struct-tag-based marshal and unmarshal helpers for typed records, instead of only string records and name-to-value maps
  • Detect and report invalid UTF-8 with a positioned error, the same way malformed quoting is reported
  • Add a csvgrep-style row filter and a dialect-conversion mode (read semicolon-delimited, write comma-delimited) to the CLI
Recommended reading

Books & references that go deeper

  • The closest thing CSV has to a grammar: how records, fields, quoting, and the doubled-quote escape are defined, and why CRLF is the canonical line ending. Keep it open beside the quoting and line-ending chapters.

  • The clearest treatment of dialects: delimiter, quoting rules, trimming, and line terminators as configurable knobs. The Dialect this project builds mirrors its design.

  • A survey of every way a naive split on commas and newlines is wrong: embedded delimiters, embedded newlines, quotes, encodings, and ragged rows. This is the CSV-is-harder-than-you-think list the project pins down one lesson at a time.

  • A production reference implementation of exactly this design: a state-machine reader with positioned errors and field-count checking, plus a writer with minimal quoting. Useful to compare your API against once each chapter lands.

  • Model for Tabular Data and Metadata on the Web · W3C CSV on the Web Working Group

    The modern attempt to give CSV real semantics: dialects, headers, and typed columns. Read it for where a library like this one goes next, beyond raw string records.