// SPDX-License-Identifier: MIT
#include <cparse/Parser.hh>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
using Row = std::vector<std::string>;
using fedem::parser::ParseError;
class CsvLite : public fedem::parser::Parser
{
public:
std::string getGrammarName() const noexcept override { return "csv-lite"; }
// Iterator-style: call nextRow() until it returns nullopt. The file
// itself is opened via the inherited Parser::openStream() — see its
// doc comment in Parser.hh for why parse() doesn't fit a grammar
// driven from outside one row at a time.
std::optional<Row> nextRow()
{
if (!getCursor().isValid()) return std::nullopt;
if (getCursor().safePeek() == '\032') return std::nullopt;
Row row;
parseRow(row);
return row;
}
// For callers who want the whole file at once.
std::vector<Row> readAll()
{
std::vector<Row> rows;
while (auto r = nextRow()) rows.push_back(std::move(*r));
return rows;
}
// Position of the last successful row's start.
unsigned long lastRowLine() const noexcept { return rowLine_; }
unsigned long lastRowCol() const noexcept { return rowCol_; }
protected:
bool start() override
{
// Never called: this grammar's entry point is openStream() +
// nextRow(), not parse(). Present only because Parser declares it
// pure.
return true;
}
private:
void parseRow(Row& out)
{
rowLine_ = getCursor().getNextLineNumber();
rowCol_ = getCursor().getNextColumnNumber();
while (true)
{
out.push_back(parseField());
char const c = getCursor().safePeek();
if (c == ',')
{
getCursor().get();
continue;
}
if (c == '\r')
{
getCursor().get();
if (getCursor().safePeek() == '\n') getCursor().get();
return;
}
if (c == '\n')
{
getCursor().get();
return;
}
return; // EOF ends the last row
}
}
std::string parseField()
{
if (getCursor().safePeek() == '"')
return parseQuotedField();
return parseUnquotedField();
}
std::string parseUnquotedField()
{
std::string s;
while (true)
{
char const c = getCursor().safePeek();
if (c == ',' || c == '\r' || c == '\n' || c == '\032') return s;
s += getCursor().safeGet();
}
}
std::string parseQuotedField()
{
unsigned long const openLine = getCursor().getNextLineNumber();
unsigned long const openCol = getCursor().getNextColumnNumber();
getCursor().get(); // consume opening "
enterScope(); // quoted-field scope
std::string s;
while (true)
{
char const c = getCursor().safePeek();
if (c == '\032')
{
recordError(ParseError::Kind::Syntax,
"unterminated quoted field",
openLine, openCol);
return s;
}
if (c == '"')
{
getCursor().get();
// Doubled "" inside a quoted field → literal "
if (getCursor().safePeek() == '"')
{
getCursor().get();
s += '"';
continue;
}
leaveScope();
return s;
}
s += getCursor().safeGet();
}
}
unsigned long rowLine_ = 0;
unsigned long rowCol_ = 0;
};
int main(int argc, char** argv)
{
if (argc < 2)
{
std::cerr << "usage: csv <file.csv>\n";
return 1;
}
CsvLite csv;
if (!csv.openStream(argv[1]))
{
std::cerr << "csv: cannot open '" << argv[1] << "'\n";
return 1;
}
size_t rowIndex = 0;
while (auto row = csv.nextRow())
{
std::cout << "row " << ++rowIndex
<< " (line " << csv.lastRowLine() << "): ";
for (size_t i = 0; i < row->size(); ++i)
{
if (i) std::cout << " | ";
std::cout << (*row)[i];
}
std::cout << '\n';
}
for (auto const& e : csv.errors())
std::cerr << e.filename << ':' << e.line << ':' << e.col
<< ": " << e.message << '\n';
}