Since the standard fgets()
does not suffice for my use cases, as it doesn't automatically enlarge the target buffer if needed, and getline()
/fgetln()
are not part of the C standard, I rolled out my own.
Code:
The code below is listed as one file for code review.
#define TEST_MAIN // Or pass this with make/gcc.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
/*
* Reads the next line from the stream pointed to by `stream`.
*
* The line is terminated by a NUL character.
* On success, the memory pointed to by `len` shall contain the length of the line
* (including the terminating NUL character).
* The returned line does not contain a newline.
*
* Upon successful completion a pointer is returned, otherwise NULL is returned.
* `fgetline()` does not distinguish between end-of-file and error; the routines
* `feof()` and `ferror()` must be used to determine which occurred.
*/
char *fgetline(FILE *stream, size_t *len)
{
clearerr(stream);
char *line = NULL;
size_t capacity = 0;
size_t size = 0;
while (true) {
if (size >= capacity) {
char *const tmp = realloc(line, capacity += BUFSIZ);
if (!tmp) {
free(line);
return NULL;
}
line = tmp;
}
if (!fgets(line + size, BUFSIZ, stream)) {
if (ferror(stream)) {
free(line);
return NULL;
}
break;
}
if (strpbrk(line, "\n\r")) {
break;
}
size += BUFSIZ;
}
if (feof(stream) && size == 0) {
free(line);
return NULL;
}
/* TODO: Shrink the memory chunk here. */
*len = strcspn(line, "\n\r");
line[*len] = '\0';
*len += 1;
return line;
}
#ifdef TEST_MAIN
#include <assert.h>
int main(int argc, char **argv)
{
assert(argv[1]);
FILE *fp = fopen(argv[1], "rb");
assert(fp);
size_t size = 0;
assert(!strcmp(fgetline(fp, &size), "12"));
assert(size == 3);
assert(!strcmp(fgetline(fp, &size), "AB"));
assert(size == 3);
assert(!strcmp(fgetline(fp, &size), "123"));
assert(size == 4);
assert(!strcmp(fgetline(fp, &size), "ABC"));
assert(size == 4);
assert(!strcmp(fgetline(fp, &size), "1234"));
assert(size == 5);
assert(!strcmp(fgetline(fp, &size), "ABCD"));
assert(size == 5);
rewind(fp);
char *res = NULL;
while ((res = fgetline(fp, &size)) != NULL) {
printf("Line read: \"%s\", line length: %zu, string length: %zu\n", res, size, strlen(res));
}
assert(!fgetline(fp, &size));
assert(feof(fp));
return EXIT_SUCCESS;
}
#endif /* TEST_MAIN */
prints:
Line read: "12", line length: 3, string length: 2
Line read: "AB", line length: 3, string length: 2
Line read: "123", line length: 4, string length: 3
Line read: "ABC", line length: 4, string length: 3
Line read: "1234", line length: 5, string length: 4
Line read: "ABCD", line length: 5, string length: 4
Line read: "", line length: 1, string length: 0
where the sample text file contained:
12
AB
123
ABC
1234
ABCD
^@^@^@^@
The last line contains 4 NUL characters, courtesy of fputc(0, fp)
.
Review Request:
Are there any bugs in the code?
Are there any edge cases I have missed?
Does the function provide what's expected of a good line-reading function?
If there are embedded NUL characters in the line, the function returns the string up to the first NUL character. Was that a bad decision to take?