From 2447bb63ccb20bc25ee1262be0d420b72cdc0c17 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Thu, 27 Apr 2023 01:34:49 +0000 Subject: [PATCH] Add hdoc, a simple tool for generating documentation from a C header. This is a very early prototype. It works, but it is probably not efficient or reliable. However, the documentation format it parses is stable, so I will begin moving the documentation into the headers. --- TODO.txt | 4 +- src/HeaderParser.c | 552 +++++++++++++++++++++++++++++++++++++ src/include/HeaderParser.h | 75 +++++ tools/src/hdoc.c | 352 +++++++++++++++++++++++ 4 files changed, 982 insertions(+), 1 deletion(-) create mode 100644 src/HeaderParser.c create mode 100644 src/include/HeaderParser.h create mode 100644 tools/src/hdoc.c diff --git a/TODO.txt b/TODO.txt index 6996353..0cfc767 100644 --- a/TODO.txt +++ b/TODO.txt @@ -37,10 +37,12 @@ Milestone: v0.3.0 [ ] HashMap [ ] HttpRouter [ ] Str + [ ] HeaderParser + [ ] hdoc [x] telodendria-admin [x] telodendria-setup [x] telodendria-config - [ ] Refactor dev pages so function description and + [~] Refactor dev pages so function description and return value are in the same location. [x] Debug memory leaks with caching diff --git a/src/HeaderParser.c b/src/HeaderParser.c new file mode 100644 index 0000000..c020d95 --- /dev/null +++ b/src/HeaderParser.c @@ -0,0 +1,552 @@ +#include + +#include + +#include +#include + +static int +HeaderConsumeWhitespace(HeaderExpr * expr) +{ + int c; + + while (1) + { + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_EOF; + expr->data.error.msg = "End of stream reached."; + expr->data.error.lineNo = expr->state.lineNo; + break; + } + + if (isspace(c)) + { + if (c == '\n') + { + expr->state.lineNo++; + } + } + else + { + break; + } + } + + return c; +} + +static char * +HeaderConsumeWord(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (!isspace(c = StreamGetc(expr->state.stream))) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +static char * +HeaderConsumeAlnum(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (isalnum(c = StreamGetc(expr->state.stream))) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +static char * +HeaderConsumeArg(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + int block = 0; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (((c = StreamGetc(expr->state.stream)) != ',' && c != ')') || block > 0) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + + if (c == '(') + { + block++; + } + else if (c == ')') + { + block--; + } + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +void +HeaderParse(Stream * stream, HeaderExpr * expr) +{ + int c; + + if (!expr) + { + return; + } + + if (!stream) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "NULL pointer to stream."; + expr->data.error.lineNo = -1; + return; + } + + if (expr->type == HP_DECLARATION && expr->data.declaration.args) + { + size_t i; + + for (i = 0; i < ArraySize(expr->data.declaration.args); i++) + { + Free(ArrayGet(expr->data.declaration.args, i)); + } + + ArrayFree(expr->data.declaration.args); + } + + expr->state.stream = stream; + if (!expr->state.lineNo) + { + expr->state.lineNo = 1; + } + + c = HeaderConsumeWhitespace(expr); + + if (StreamEof(stream) || StreamError(stream)) + { + expr->type = HP_EOF; + expr->data.error.msg = "End of stream reached."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == '/') + { + int i = 0; + + c = StreamGetc(expr->state.stream); + if (c != '*') + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected comment opening."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + expr->type = HP_COMMENT; + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit exceeded while parsing comment."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated comment."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == '*') + { + c = StreamGetc(expr->state.stream); + if (c == '/') + { + expr->data.text[i] = '\0'; + break; + } + else + { + expr->data.text[i] = '*'; + i++; + expr->data.text[i] = c; + i++; + } + } + else + { + expr->data.text[i] = c; + i++; + + if (c == '\n') + { + expr->state.lineNo++; + } + } + } + } + else if (c == '#') + { + int i = 0; + char *word; + + expr->type = HP_PREPROCESSOR_DIRECTIVE; + expr->data.text[i] = '#'; + i++; + + word = HeaderConsumeWord(expr); + + strncpy(expr->data.text + i, word, HEADER_EXPR_MAX - i - 1); + i += strlen(word); + + if (strcmp(word, "include") == 0 || + strcmp(word, "undef") == 0 || + strcmp(word, "ifdef") == 0 || + strcmp(word, "ifndef") == 0) + { + /* Read one more word */ + Free(word); + word = HeaderConsumeWord(expr); + + if (i + strlen(word) + 1 >= HEADER_EXPR_MAX) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit reached parsing preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + strncpy(expr->data.text + i + 1, word, HEADER_EXPR_MAX - i - 1); + expr->data.text[i] = ' '; + } + + Free(word); + } + else if (strcmp(word, "define") == 0 || + strcmp(word, "if") == 0 || + strcmp(word, "elif") == 0 || + strcmp(word, "error") == 0) + { + Free(word); + expr->data.text[i] = ' '; + i++; + + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit reached parsing preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + /* TODO: Handle backslash escapes */ + if (c == '\n') + { + expr->data.text[i] = '\0'; + break; + } + else + { + expr->data.text[i] = c; + i++; + } + } + } + else if (strcmp(word, "else") == 0 || + strcmp(word, "endif") == 0) + { + /* Read no more words, that's the whole directive */ + } + else + { + Free(word); + + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unknown preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + } + } + else + { + char *word; + + StreamUngetc(expr->state.stream, c); + word = HeaderConsumeWord(expr); + + if (strcmp(word, "typedef") == 0) + { + int block = 0; + int i = 0; + + expr->type = HP_TYPEDEF; + strncpy(expr->data.text, word, HEADER_EXPR_MAX - 1); + i += strlen(word); + expr->data.text[i] = ' '; + i++; + + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit exceeded while parsing typedef."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated typedef."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + expr->data.text[i] = c; + i++; + + if (c == '{') + { + block++; + } + else if (c == '}') + { + block--; + } + + if (block <= 0 && c == ';') + { + expr->data.text[i] = '\0'; + break; + } + } + } + else if (strcmp(word, "extern") == 0) + { + int wordLimit = sizeof(expr->data.declaration.returnType) - 8; + int wordLen; + + Free(word); + + word = HeaderConsumeWord(expr); + wordLen = strlen(word); + if (wordLen > wordLimit) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Return of declaration exceeds length limit."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + int i = wordLen; + + expr->type = HP_DECLARATION; + strncpy(expr->data.declaration.returnType, word, wordLimit); + + Free(word); + + c = HeaderConsumeWhitespace(expr); + if (c == '*') + { + expr->data.declaration.returnType[i] = ' '; + i++; + expr->data.declaration.returnType[i] = '*'; + i++; + while ((c = HeaderConsumeWhitespace(expr)) == '*') + { + expr->data.declaration.returnType[i] = c; + i++; + } + } + + StreamUngetc(expr->state.stream, c); + word = HeaderConsumeAlnum(expr); + + wordLen = strlen(word); + wordLimit = sizeof(expr->data.declaration.name) - 1; + + if (wordLen > wordLimit) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Function name too long."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + strncpy(expr->data.declaration.name, word, wordLimit); + Free(word); + word = NULL; + + c = HeaderConsumeWhitespace(expr); + if (c != '(') + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected '('"; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + expr->data.declaration.args = ArrayCreate(); + + do { + word = HeaderConsumeArg(expr); + ArrayAdd(expr->data.declaration.args, word); + word = NULL; + } + while ((!StreamEof(expr->state.stream)) && ((c = HeaderConsumeWhitespace(expr)) != ')')); + + if (StreamEof(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "End of file reached before ')'."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = HeaderConsumeWhitespace(expr); + if (c != ';') + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected ';'."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + } + } + } + else + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected comment, typedef, or extern."; + expr->data.error.lineNo = expr->state.lineNo; + } + + Free(word); + } +} diff --git a/src/include/HeaderParser.h b/src/include/HeaderParser.h new file mode 100644 index 0000000..072db25 --- /dev/null +++ b/src/include/HeaderParser.h @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef TELODENDRIA_HEADERPARSER_H +#define TELODENDRIA_HEADERPARSER_H + +#include +#include + +/* Here's a comment */ +#define HEADER_EXPR_MAX 4096 + +typedef enum HeaderExprType +{ + HP_COMMENT, + HP_PREPROCESSOR_DIRECTIVE, + HP_TYPEDEF, + HP_DECLARATION, + HP_SYNTAX_ERROR, + HP_PARSE_ERROR, + HP_EOF +} HeaderExprType; + +typedef struct HeaderDeclaration +{ + char returnType[64]; + char name[32]; /* Enforced by ANSI C */ + Array *args; +} HeaderDeclaration; + +typedef struct HeaderExpr +{ + HeaderExprType type; + union + { + char text[HEADER_EXPR_MAX]; + HeaderDeclaration declaration; + struct + { + int lineNo; + char *msg; + } error; + } data; + + struct + { + Stream *stream; + int lineNo; + } state; +} HeaderExpr; + +extern void +HeaderParse(Stream *, HeaderExpr *); + +#endif /* TELODENDRIA_HEADERPARSER_H */ diff --git a/tools/src/hdoc.c b/tools/src/hdoc.c new file mode 100644 index 0000000..3772401 --- /dev/null +++ b/tools/src/hdoc.c @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include + +#include +#include +#include + +#include +#include +#include +#include + +typedef struct DocDecl +{ + char docs[HEADER_EXPR_MAX]; + HeaderDeclaration decl; +} DocDecl; + +typedef struct DocTypedef +{ + char docs[HEADER_EXPR_MAX]; + char text[HEADER_EXPR_MAX]; +} DocTypedef; + +static void +ParseMainBlock(HashMap * registers, Array * descr, char *comment) +{ + char *line = strtok(comment, "\n"); + + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (!*line) + { + goto next; + } + + if (*line == '@') + { + int i = 0; + + line++; + + while (!isspace(line[i])) + { + i++; + } + + line[i] = '\0'; + + Free(HashMapSet(registers, line, StrDuplicate(line + i + 1))); + } + else + { + ArrayAdd(descr, StrDuplicate(line)); + } + +next: + line = strtok(NULL, "\n"); + } +} + +int +main(int argc, char **argv) +{ + HeaderExpr expr; + size_t i; + char *val; + int exit = EXIT_SUCCESS; + + HashMap *registers = HashMapCreate(); + Array *descr = ArrayCreate(); + + Array *declarations = ArrayCreate(); + DocDecl *decl = NULL; + + Array *typedefs = ArrayCreate(); + DocTypedef *type = NULL; + + char comment[HEADER_EXPR_MAX]; + int isDocumented = 0; + + memset(&expr, 0, sizeof(expr)); + + while (1) + { + HeaderParse(StreamStdin(), &expr); + + switch (expr.type) + { + case HP_PREPROCESSOR_DIRECTIVE: + /* Ignore */ + break; + case HP_EOF: + /* Done parsing */ + goto last; + case HP_PARSE_ERROR: + case HP_SYNTAX_ERROR: + StreamPrintf(StreamStderr(), "Parse Error: (line %d) %s\n", + expr.data.error.lineNo, expr.data.error.msg); + exit = EXIT_FAILURE; + goto finish; + case HP_COMMENT: + if (expr.data.text[0] != '*') + { + break; + } + + if (strncmp(expr.data.text, "**", 2) == 0) + { + ParseMainBlock(registers, descr, expr.data.text); + } + else + { + strncpy(comment, expr.data.text, sizeof(comment)); + isDocumented = 1; + } + break; + case HP_TYPEDEF: + if (!isDocumented) + { + StreamPrintf(StreamStderr(), + "Error: Undocumented typedef:\n%s\n", + expr.data.text); + exit = EXIT_FAILURE; + goto finish; + } + else + { + type = Malloc(sizeof(DocTypedef)); + strncpy(type->docs, comment, sizeof(type->docs)); + strncpy(type->text, expr.data.text, sizeof(type->text)); + ArrayAdd(typedefs, type); + isDocumented = 0; + } + break; + case HP_DECLARATION: + if (!isDocumented) + { + StreamPrintf(StreamStderr(), + "Error: %s() is undocumented.\n", + expr.data.declaration.name); + exit = EXIT_FAILURE; + goto finish; + } + else + { + decl = Malloc(sizeof(DocDecl)); + decl->decl = expr.data.declaration; + decl->decl.args = ArrayCreate(); + strncpy(decl->docs, comment, sizeof(decl->docs)); + for (i = 0; i < ArraySize(expr.data.declaration.args); i++) + { + ArrayAdd(decl->decl.args, StrDuplicate(ArrayGet(expr.data.declaration.args, i))); + } + ArrayAdd(declarations, decl); + isDocumented = 0; + } + break; + default: + StreamPrintf(StreamStderr(), "Unknown header type: %d\n", expr.type); + StreamPrintf(StreamStderr(), "This is a programming error.\n"); + exit = EXIT_FAILURE; + goto finish; + } + } + +last: + val = HashMapGet(registers, "Nm"); + if (!val) + { + HashMapSet(registers, "Nm", StrDuplicate("Unnamed")); + } + + val = HashMapGet(registers, "Dd"); + if (!val) + { + time_t currentTime; + struct tm *timeInfo; + char tsBuf[1024]; + + currentTime = time(NULL); + timeInfo = localtime(¤tTime); + strftime(tsBuf, sizeof(tsBuf), "%B %d %Y", timeInfo); + + val = tsBuf; + } + StreamPrintf(StreamStdout(), ".Dd $%s: %s $\n", "Mdocdate", val); + + val = HashMapGet(registers, "Os"); + if (val) + { + StreamPrintf(StreamStdout(), ".Os %s\n", val); + } + + val = HashMapGet(registers, "Nm"); + StreamPrintf(StreamStdout(), ".Dt %s 3\n", val); + StreamPrintf(StreamStdout(), ".Sh NAME\n"); + StreamPrintf(StreamStdout(), ".Nm %s\n", val); + + val = HashMapGet(registers, "Nd"); + if (!val) + { + val = "No Description."; + } + StreamPrintf(StreamStdout(), ".Nd %s\n", val); + + StreamPrintf(StreamStdout(), ".Sh SYNOPSIS\n"); + val = HashMapGet(registers, "Nm"); + StreamPrintf(StreamStdout(), ".In %s.h\n", val); + for (i = 0; i < ArraySize(declarations); i++) + { + size_t j; + + decl = ArrayGet(declarations, i); + StreamPrintf(StreamStdout(), ".Ft %s\n", decl->decl.returnType); + StreamPrintf(StreamStdout(), ".Fn %s ", decl->decl.name); + for (j = 0; j < ArraySize(decl->decl.args); j++) + { + StreamPrintf(StreamStdout(), "\"%s\" ", ArrayGet(decl->decl.args, j)); + } + StreamPutc(StreamStdout(), '\n'); + } + + if (ArraySize(typedefs)) + { + StreamPrintf(StreamStdout(), ".Sh TYPE DECLARATIONS\n"); + for (i = 0; i < ArraySize(typedefs); i++) + { + char *line; + + type = ArrayGet(typedefs, i); + StreamPrintf(StreamStdout(), ".Bd -literal -offset indent\n"); + StreamPrintf(StreamStdout(), "%s\n", type->text); + StreamPrintf(StreamStdout(), ".Ed\n.Pp\n"); + + line = strtok(type->docs, "\n"); + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (*line) + { + StreamPrintf(StreamStdout(), "%s\n", line); + } + + line = strtok(NULL, "\n"); + } + } + } + + StreamPrintf(StreamStdout(), ".Sh DESCRIPTION\n"); + for (i = 0; i < ArraySize(descr); i++) + { + StreamPrintf(StreamStdout(), "%s\n", ArrayGet(descr, i)); + } + + for (i = 0; i < ArraySize(declarations); i++) + { + size_t j; + char *line; + + decl = ArrayGet(declarations, i); + StreamPrintf(StreamStdout(), ".Ss %s %s(", + decl->decl.returnType, decl->decl.name); + for (j = 0; j < ArraySize(decl->decl.args); j++) + { + StreamPrintf(StreamStdout(), "%s", ArrayGet(decl->decl.args, j)); + if (j < ArraySize(decl->decl.args) - 1) + { + StreamPuts(StreamStdout(), ", "); + } + StreamPuts(StreamStdout(), ")\n"); + } + + line = strtok(decl->docs, "\n"); + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (*line) + { + StreamPrintf(StreamStdout(), "%s\n", line); + } + + line = strtok(NULL, "\n"); + } + } + + val = HashMapGet(registers, "Xr"); + if (val) + { + char *xr = strtok(val, " "); + + StreamPrintf(StreamStdout(), ".Sh SEE ALSO\n"); + while (xr) + { + if (*xr) + { + StreamPrintf(StreamStdout(), ".Xr %s 3 ", xr); + } + + xr = strtok(NULL, " "); + + if (xr) + { + StreamPutc(StreamStdout(), ','); + } + StreamPutc(StreamStdout(), '\n'); + } + } + +finish: + StreamClose(StreamStdin()); + StreamClose(StreamStdout()); + StreamClose(StreamStderr()); + + MemoryFreeAll(); + return exit; +}