add tag
JeT
**Context**

Once I spent quite a long time passing all my exams (many MCQ) to `exam` class... Exams are now 100% online... (sigh)

I need now to provide a `csv` where the questions of the MCQ below 

```
\documentclass[12pt,addpoints]{exam}
\usepackage[a4paper, margin=1.80cm]{geometry}

\begin{document}
\begin{questions}
 \question What is the answer ? 
 \begin{oneparchoices}
 \choice 70
 \choice 75
 \choice 80
 \CorrectChoice 85
 \choice None of the above
 \end{oneparchoices}
\end{questions}
\end{document}
```
would be diplayed like (**Inc**orrect,**Cor**rect, just to be clear :)
`question,answer1,Cor/Inc,answer2,Cor/Inc,answer3,Cor/Inc	,answer4,Cor/Inc,answer5,Cor/Inc`

And it would render like

`What is the answer ?,Inc,70,Inc,75,Inc,80,Inc,85,Cor,None of the above,Inc`

Each line would obviously be a new question.
No parts or subquestions.
Top Answer
FoggyFinder
Here is a slightly changed version of what I've posted in the comments section:

First, we describe our data types:

```text/x-fsharp
[<RequireQualifiedAccess>]
type QOptionType =
    | Correct
    | Incorrect
    with 
        override x.ToString() =
            match x with
            | Correct -> "Cor"
            | Incorrect -> "Inc"

type QOption = {
    Type : QOptionType
    Value : string
}

type Question = {
    Title : string
    Options : QOption list
}
```

Then, we have to write a function that consumes file's content and produces list of the `Question` for it.

```text/x-fsharp
module private Parser =
    open FParsec
    
    let ws = spaces
    let str_ws s = pstring s .>> ws

    let tillNewline = manyCharsTill anyChar newline .>> ws

    let questionTitle = str_ws "\\question" >>. tillNewline |>> string

    let pChoiceIncorrect = 
        str_ws "\\choice"  >>. tillNewline
        |>> fun s -> { Value = s; Type = QOptionType.Incorrect }

    let pChoiceCorrect = 
        str_ws "\\CorrectChoice" >>. tillNewline
        |>> fun s -> { Value = s; Type = QOptionType.Correct }
    
    let choicesParser = 
        let pOpen = str_ws "\\begin{oneparchoices}"
        let pEnd = str_ws "\\end{oneparchoices}"

        let p = [ pChoiceCorrect ; pChoiceIncorrect ] |> choice |> many
        between pOpen pEnd p
            
    let questionParser = 
        let pOpen = str_ws "\\begin{questions}"
        let pEnd = str_ws "\\end{questions}"

        let p = 
            pipe2 questionTitle choicesParser (fun title options ->
                {
                    Title = title
                    Options = options
                })
            |> many

        between pOpen pEnd p
        
    let tillDocumentBegin =
        str_ws "\\begin{document}"
        |> manyCharsTill anyChar

    let parser = 
        tillDocumentBegin >>. questionParser .>> str_ws "\\end{document}" .>> eof

    let parse text =
        match run parser text with
        | ParserResult.Success (result,_,_) ->
            Result.Ok result
        | ParserResult.Failure (error,_,_) -> 
            Result.Error error
```


I used the wonderful [FParsec](https://www.quanttec.com/fparsec/) library since it's a fun. You can replace the `Parser` module with something more straightforward (or opposite more complicated).

The next step would be transforming a sequence of questions into a csv file.

```text/x-fsharp
[<RequireQualifiedAccess>]
module CSV = 
    open CsvHelper
    open System.IO
    open System.Globalization
    
    let writeData path (data:seq<Question>) = 
        use sw = new StreamWriter(File.Create path)
        use csv = new CsvWriter(sw, CultureInfo.InvariantCulture)
        for q in data do
            csv.WriteField(q.Title, false)
            for opt in q.Options do
                csv.WriteField(opt.Value)
                csv.WriteField(opt.Type.ToString())
            csv.NextRecord()
```

I used the [CsvHelper](https://joshclose.github.io/CsvHelper/getting-started/) library for that purpose. But, again, a simple string can be created manually if you wish.

And finally we have to enumerate all files in some folder, next take only appropriate and, at last, handle them: 

```text/x-fsharp
open System.IO

let proceedData folder = 
    Directory.EnumerateFiles(folder, "*.*", SearchOption.TopDirectoryOnly)
    |> Seq.filter (fun path -> Path.GetExtension(path) = ".tex")
    |> Seq.iter(fun path ->
        match File.ReadAllText path |> Parser.parse with
        | Result.Ok qs -> 
            let name = Path.GetFileNameWithoutExtension(path)
            let csvName = name + ".csv"
            let csvPath = Path.Combine(Path.GetDirectoryName(path), csvName)
                
            CSV.writeData csvPath qs

            printfn "%s is handled" csvPath
        | Result.Error error ->
            printfn "%s eccor occured:" path
            printfn "%s" error)

let sampleFolderPath = "D:\\TestFolder\\TestData"

sampleFolderPath
|> proceedData
```

Link to the [gist](https://gist.github.com/FoggyFinder/fb8444ed0bba870dceaf9fc7aa80c85e) with full code

Conclusion: F# fits perfectly for such tasks :-)
Answer #2
wizzwizz4
Originally on [TopAnswers Python](https://topanswers.xyz/python?q=1540#a1794). This code doesn't really parse TeX; it reads files formatted _exactly_ how you've written your document. It's also very messy.

It's iterator-based, though, meaning you can extend it to implement nearly any “parsing” you like – provided that it can be done on a line-by-line basis, and that you don't need lookahead (e.g. your “exit block” lines can be discarded).

The important thing is that it works. :-)

```python
import csv
from functools import partial
from itertools import takewhile

def questions(lines):
    lines = iter(lines)
    for line in lines:
        if r'\begin{questions}' in line:
            break
    while True:
        for line in lines:
            if r'\end{questions}' in line:
                return
            q = line.split(r'\question', maxsplit=1)
            if len(q) == 2:
                question = q[1].strip()
                break
        if r'\begin{oneparchoices}' not in next(lines):
            raise ValueError(r"Expected \begin{oneparchoices}")
        yield question, tuple(
            (answer.strip(), r'Correct' in choice)
            for choice, answer in map(
                partial(str.split, maxsplit=1),
                takewhile(
                    lambda line: r'\end{oneparchoices}' not in line,
                    lines
                )
            )
        )

def q_flatten(questions):
    for question, answers in questions:
        yield (question,) + tuple(a_flatten(answers))

def a_flatten(answers):
    for answer, correct in answers:
        yield answer
        if correct:
            yield 'Cor'
        else:
            yield 'Inc'

with open('MCQ.tex') as in_, \
     open('FlattenMCQ.csv', 'w', newline='') as out:
    writer = csv.writer(out)
    # If you want a header, add it here.
    ##writer.writerow(("column1", "column2", "etc."))
    writer.writerows(q_flatten(questions(in_)))
```

Enter question or answer id or url (and optionally further answer ids/urls from the same question) from

Separate each id/url with a space. No need to list your own answers; they will be imported automatically.