Written from a line-by-line source review; every example output is from a real run.
Introduction
The csv plugin is a self-contained, dependency-free CSV/TSV reader and writer. It parses CSV text into a table from which you can read by row and column (cell) or by column name, or you can build a table from scratch and serialize it to RFC 4180 CSV text. Both the parser and the serializer are implemented inside the plugin.
A handle-based model
Like the JSON plugin, this plugin is handle-based: Parse / ParseEx / NewTable return an integer identifier (a handle) that refers to an internal table. Every further operation takes this handle as its first argument. When you are done, release it with Free. At most 64 tables can be open at once.
Header rows and name-based access
Multi-row headers are first-class: the headerRows parameter of ParseEx says how many rows are header (0 = no header, 1 = classic, 2+ = stacked headers, such as a group row plus a unit row). RowCount counts ONLY the data rows (after the header rows). Name-based access (GetByName / ColIndex / HeaderName) uses one designated “name row”; by default that is the LAST header row (usually the most specific column label), but SetNameRow can change it.
RFC 4180 quoting
On input, fields may be wrapped in double quotes; inside a quoted field a doubled quote ("") is a single literal quote, and the delimiter and newlines are literal. On output, a field is quoted only if it contains the delimiter, a quote, CR or LF. Both line endings (\n and \r\n) are accepted on input; the output uses \n.
Loading the plugin
plugin "../plugins/print/PrintPlugin"; plugin "../plugins/csv/CsvPlugin";
A typical flow — reading
i32 $h;
$h = csv.ParseEx("name;age\nAda;36", ";", 1);
printf("%s\n", csv.GetByName($h, 0, "age"));
csv.Free($h);What to know about every function (the basics)
The row index is 0-based and refers to DATA rows: data row 0 is the first non-header row. The column index is also 0-based.
An out-of-range cell read is not an error: Get / GetByName / HeaderCell / HeaderName return an empty string, and ColIndex returns -1 for an unknown name. So the script need not guard every read.
An invalid handle, however, is an error: if you pass the handle of a non-existent or already-freed table, the script stops with a runtime error. So do not reuse a handle after Free.
Ragged (unequal-length) rows are padded by the parser to the widest row: the missing cells read as empty strings.
On writing, NewTable fixes the column count; SetCell writes only to an existing data row and a valid column (otherwise it returns 0 rather than failing).
How to read the signatures
handle is always the table's identifier. The type after the -> arrow is the return type. For example, csv.Get(handle, row, col) -> string takes three arguments and returns a string.
Parsing
Loading CSV text into a table. Both functions return a handle.
csv.Parse
csv.Parse(text) -> int
Parses CSV text with a comma delimiter and no header row (every row is a data row).
| Parameter | Type | Description |
|---|---|---|
| text | string | The CSV text (rows separated by \n or \r\n). |
The table handle (a positive integer). Out of memory or more than 64 open tables causes a runtime error.
i32 $h;
$h = csv.Parse("a,b,c\n1,2,3\n4,5,6");
printf("%d\n", csv.RowCount($h));
printf("%s\n", csv.Get($h, 1, 2));
csv.Free($h);3 3
For simple, header-less, comma-separated data. If you need a header or a different delimiter, use ParseEx.
csv.ParseEx
csv.ParseEx(text, delim, headerRows) -> int
Parses CSV text with any single-character delimiter and a given number of header rows.
| Parameter | Type | Description |
|---|---|---|
| text | string | The CSV/TSV text. |
| delim | string | The delimiter — exactly one character (for example ",", ";" or a tab). |
| headerRows | int | The number of header rows (0 = none, 1 = classic, 2+ = stacked). A negative value is clamped to 0. |
The table handle. If the delimiter is not exactly one character, a runtime error.
i32 $h;
$h = csv.ParseEx("name;age\nAda;36\nBob;41", ";", 1);
printf("%d\n", csv.RowCount($h));
printf("%s\n", csv.GetByName($h, 1, "age"));
csv.Free($h);2 41
For TSV (delim = tab), European semicolon CSV, or wherever you want to refer to columns by their header. RowCount here counts only the data rows.
Lifecycle
Releasing the table when you no longer need it.
csv.Free
csv.Free(handle) -> int
Frees the table and its memory; the handle becomes invalid afterwards.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The handle of the table to free. |
1 if it was freed; 0 if the handle is invalid or was already freed.
i32 $h;
$h = csv.Parse("a,b\n1,2");
printf("%d\n", csv.Free($h));
printf("%d\n", csv.Free($h));1 0
Always call it when you are done with a table — otherwise you may hit the limit of 64. Free is the forgiving operation: it does not fail on an invalid handle, it just returns 0.
Inspection (dimensions)
Querying the table's dimensions.
csv.RowCount
csv.RowCount(handle) -> int
Returns the number of DATA rows (excluding the header rows).
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
The number of data rows.
i32 $h;
// 1 header row + 2 data rows
$h = csv.ParseEx("name;age\nAda;36\nBob;41", ";", 1);
printf("%d\n", csv.RowCount($h));
csv.Free($h);2
For iterating over the data rows: from 0 to RowCount-1. The header does not count.
csv.ColCount
csv.ColCount(handle) -> int
Returns the number of columns.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
The number of columns.
i32 $h;
$h = csv.Parse("a,b,c\n1,2,3");
printf("%d\n", csv.ColCount($h));
csv.Free($h);3
For iterating over columns, or for checking the shape of the table after parsing.
csv.HeaderRowCount
csv.HeaderRowCount(handle) -> int
Returns the number of header rows (as you gave it to ParseEx).
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
The number of header rows (0 if there is no header).
i32 $h;
$h = csv.ParseEx("Sales,Sales\nQ1,Q2\n10,20", ",", 2);
printf("%d\n", csv.HeaderRowCount($h));
csv.Free($h);2
When you want to check whether a table of unknown origin has a header before trying to read by name.
Setting the name row
Choosing which header row supplies the column names for name-based access.
csv.SetNameRow
csv.SetNameRow(handle, headerRow) -> int
Sets which header row provides the column names for name-based access (GetByName / ColIndex / HeaderName). The default is the last header row.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| headerRow | int | The 0-based index of the header row to use as the name row. |
1 if it was set; 0 if headerRow is out of range (in which case nothing changes).
i32 $h;
$h = csv.ParseEx("Sales,Sales,HR\nQ1,Q2,Q1\n10,20,5", ",", 2);
// by default the last header row is the name row (Q1/Q2)
printf("%s\n", csv.HeaderName($h, 1));
csv.SetNameRow($h, 0);
printf("%s\n", csv.HeaderName($h, 1));
csv.Free($h);Q2 Sales
For stacked headers, when you want to refer to columns by the upper group row rather than the lowest (most specific) row.
Reading
Reading cells by index or by column name. An out-of-range / unknown reference returns an empty string (or -1 for ColIndex).
csv.Get
csv.Get(handle, row, col) -> string
Returns the value of a data cell by row and column index.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| row | int | The 0-based data-row index (counted without the header). |
| col | int | The 0-based column index. |
The cell's text; an empty string if the row or column is out of range.
i32 $h;
$h = csv.Parse("a,b,c\n1,2,3\n4,5,6");
printf("%s\n", csv.Get($h, 0, 0));
printf("%s\n", csv.Get($h, 1, 2));
printf("[%s]\n", csv.Get($h, 9, 9));
csv.Free($h);a 3 []
For reading by position, when there is no header, or for a fast index-based walk over the table.
csv.GetByName
csv.GetByName(handle, row, col) -> string
Returns the value of a data cell by row index and COLUMN NAME (per the name row).
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| row | int | The 0-based data-row index. |
| col | string | The column's name in the name row. |
The cell's text; an empty string if the name is unknown or the row is out of range.
i32 $h;
$h = csv.ParseEx("name;age\nAda;36\nBob;41", ";", 1);
printf("%s\n", csv.GetByName($h, 0, "name"));
printf("%s\n", csv.GetByName($h, 1, "age"));
printf("[%s]\n", csv.GetByName($h, 0, "nope"));
csv.Free($h);Ada 41 []
The most readable choice for tables with a header: you refer to a column by name regardless of column order. If you read from the same column often, fetch its index once with ColIndex, then read with Get.
csv.HeaderCell
csv.HeaderCell(handle, headerRow, col) -> string
Returns the value of a header cell by header-row and column index.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| headerRow | int | The 0-based header-row index. |
| col | int | The 0-based column index. |
The header cell's text; an empty string if the header row or column is out of range.
i32 $h;
$h = csv.ParseEx("Sales,Sales,HR\nQ1,Q2,Q1\n10,20,5", ",", 2);
printf("%s\n", csv.HeaderCell($h, 0, 0));
printf("%s\n", csv.HeaderCell($h, 1, 1));
csv.Free($h);Sales Q2
For stacked headers, where you can reach any cell of any header row (for example to print the upper group row and the lower unit row together).
csv.HeaderName
csv.HeaderName(handle, col) -> string
Returns a column's name from the designated name row.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| col | int | The 0-based column index. |
The column's name in the name row; an empty string if there is no header or the column is out of range.
i32 $h;
$h = csv.ParseEx("name;age\nAda;36", ";", 1);
printf("%s\n", csv.HeaderName($h, 0));
printf("%s\n", csv.HeaderName($h, 1));
csv.Free($h);name age
For printing column headers, or when you walk columns by index but also want to show their names.
csv.ColIndex
csv.ColIndex(handle, col) -> int
Gives which column corresponds to the given name (per the name row).
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| col | string | The column name to look for. |
The column's 0-based index, or -1 if there is no column with that name.
i32 $h;
$h = csv.ParseEx("name;age\nAda;36", ";", 1);
printf("%d\n", csv.ColIndex($h, "age"));
printf("%d\n", csv.ColIndex($h, "nope"));
csv.Free($h);1 -1
Fetch a column's index once, then read with the faster Get inside the loop — so you do not resolve the name for every cell. The -1 signals a missing column.
Writing
Building a table from scratch, cell by cell, then serializing it to CSV text.
csv.NewTable
csv.NewTable(colCount, headerRows) -> int
Creates a new, empty table with a fixed column count and a given number of (pre-created, empty) header rows.
| Parameter | Type | Description |
|---|---|---|
| colCount | int | The number of columns (at least 1). |
| headerRows | int | The number of header rows (0 or more). A negative value is clamped to 0. |
The new table's handle. If colCount is invalid (0 or too large), a runtime error.
i32 $h;
$h = csv.NewTable(2, 1);
printf("%d\n", csv.ColCount($h));
printf("%d\n", csv.HeaderRowCount($h));
csv.Free($h);2 1
The first step of building output CSV. The header rows are created with empty cells; fill them with SetHeaderCell.
csv.SetHeaderCell
csv.SetHeaderCell(handle, headerRow, col, name) -> int
Sets a header cell at a given header row and column.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| headerRow | int | The 0-based header-row index. |
| col | int | The 0-based column index. |
| name | string | The header text to write. |
1 if it was written; 0 if the header row or column is out of range.
i32 $h;
$h = csv.NewTable(2, 1);
printf("%d\n", csv.SetHeaderCell($h, 0, 0, "name"));
printf("%s\n", csv.HeaderName($h, 0));
csv.Free($h);1 name
For setting the column titles of the output table. For stacked headers, fill each header row separately.
csv.AddRow
csv.AddRow(handle) -> int
Appends a new, empty data row to the end of the table.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
The new data row's 0-based index (excluding the header rows).
i32 $h;
i32 $r;
$h = csv.NewTable(2, 1);
$r = csv.AddRow($h);
printf("%d\n", $r);
csv.Free($h);0
Call it before each new record; use the returned index to write the cells with SetCell. The first data row's index is always 0, regardless of the header rows.
csv.SetCell
csv.SetCell(handle, row, col, value) -> int
Sets a data cell by row and column index.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
| row | int | The 0-based data-row index. |
| col | int | The 0-based column index. |
| value | string | The value to write. |
1 if it was written; 0 if the row or column is out of range (the row must already exist — call AddRow first).
i32 $h;
$h = csv.NewTable(2, 0);
csv.AddRow($h);
printf("%d\n", csv.SetCell($h, 0, 0, "Ada"));
printf("%d\n", csv.SetCell($h, 9, 9, "x"));
csv.Free($h);1 0
For filling a row's cells after AddRow. It does not write to a non-existent row (returns 0), so always create the row first.
csv.Serialize
csv.Serialize(handle) -> string
Serializes the whole table (header + data rows) to RFC 4180 CSV text with a comma delimiter.
| Parameter | Type | Description |
|---|---|---|
| handle | int | The table handle. |
The CSV text. A field is quoted only if it contains a comma, a quote, CR or LF.
i32 $h;
$h = csv.NewTable(2, 1);
csv.SetHeaderCell($h, 0, 0, "name");
csv.SetHeaderCell($h, 0, 1, "note");
csv.AddRow($h);
csv.SetCell($h, 0, 0, "Ada");
// quoted because of the comma
csv.SetCell($h, 0, 1, "hi, there");
printf("%s", csv.Serialize($h));
csv.Free($h);name,note Ada,"hi, there"
For emitting the built table (before saving to a file or sending over the network). Quoting is handled automatically; you only supply the raw values.
Practical notes
What the csv plugin is good for
Reading and processing tabular data (exported reports, measurement logs, configuration lists).
Name-based access to data with a header, so the code is independent of column order.
Producing output CSV that you can then save to a file or hand to another tool (such as a spreadsheet).
Handling TSV and semicolon CSV, by specifying the delimiter.
Efficient patterns
For repeated column reads, fetch ColIndex once, then read with the faster Get inside the loop — not GetByName for every cell.
Fetch RowCount/ColCount once before the loop, not on every iteration.
Always close handles with Free; because of the limit of 64, releasing is especially important in long-running scripts.
Common pitfalls
Row indexing: RowCount and Get count DATA rows; the header is not row 0. For header cells use HeaderCell/HeaderName.
Invalid handle: after Free (or with a wrong handle) the dimension and read operations raise a runtime error — unlike an out-of-range cell read, which only returns an empty string.
Single-character delimiter: the delim parameter of ParseEx must be exactly one character; a multi- or zero-character delimiter is a runtime error.
SetCell order: AddRow first, then SetCell; SetCell on a non-existent row returns 0 and writes nothing.
Error handling
A wrong argument count or type, an invalid handle, a bad delimiter, and out of memory are reported by the runtime as a runtime error, and the script stops. Content-level cases (an out-of-range cell, an unknown column name), however, are handled by the plugin with a neutral return value: an empty string, or ColIndex's -1. So the usual reads need no extra guard checks.
The queue message-queue plugin
Thread-safe FIFO queue — complete function reference