Code View

// 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';
}