thecodingidiot.com

The ReaderThe Line Loop

The Line Loop

The prior pages established the building blocks: read() supplies raw bytes in chunks; static char *leftover preserves bytes between calls; tci_strndup handles line extraction. This page assembles them into tci_getline — and introduces one more private helper.

Helper: strjoin

tci_getline needs to concatenate leftover with each new chunk from read(). This is a single use-case in a single file — there is no other caller in libtci, and the operation has no libc equivalent to mirror. A function used in exactly one place with no standard counterpart stays static. It does not belong in libtci, and it does not yet belong in a library. Add it as a private helper in tci_getline.c:

static char *strjoin(const char *s1, const char *s2)
{
    char    *result;
    size_t  len;         /* combined length of both strings */
    size_t  i;
    size_t  j;
 
    if (!s1 || !s2)
        return (NULL);
    len = tci_strlen(s1) + tci_strlen(s2);
    result = tci_calloc(len + 1, 1);  /* +1 for null terminator */
    if (!result)
        return (NULL);
    i = 0;
    while (s1[i]) {
        result[i] = s1[i];
        i++;
    }
    j = 0;
    while (s2[j]) {                  /* append s2 after s1 */
        result[i + j] = s2[j];
        j++;
    }
    return (result);                 /* caller owns this allocation */
}

When a helper like this starts appearing in multiple files, or proves useful beyond its original context, that is the moment to promote it to a library with its own name and prefix.

Helper: extract_line

When tci_strchr(leftover, '\n') finds a newline, extract_line splits leftover at that position. It returns the line (up to and including the '\n') and updates leftover to whatever comes after:

static char *extract_line(char **leftover, char *nl)
{
    char    *line;
    char    *remainder;
 
    line = tci_strndup(*leftover, nl - *leftover + 1); /* up to and including '\n' */
    remainder = tci_strdup(nl + 1);  /* everything after the '\n' */
    free(*leftover);                /* old leftover is replaced */
    *leftover = remainder;
    return (line);                  /* caller owns this allocation */
}

nl - *leftover + 1 is the length of the prefix including the '\n'. The remainder starts at nl + 1 — the character after the newline.

Helper: flush_leftover

When read() returns 0 or −1, there is no more data. If leftover is non-empty it is the last line — a file that ends without '\n'. Return it as-is; the caller gets the string and the allocation:

static char *flush_leftover(char **leftover)
{
    char    *last;
 
    if (!*leftover || !**leftover) { /* NULL or empty string — nothing left */
        free(*leftover);
        *leftover = NULL;
        return (NULL);
    }
    last = *leftover;               /* last line has no trailing '\n' */
    *leftover = NULL;               /* next call starts clean */
    return (last);                  /* caller owns this allocation */
}

!**leftover checks whether the string is empty — leftover points to an empty "" after the last '\n' in a well-terminated file. In that case there is no more content to return.

The main function

Replace the stub in tci_getline.c with the complete implementation:

#include "libtci.h"
#include <unistd.h>  /* read */
#include <stdlib.h>  /* free */
 
#ifndef BUFFER_SIZE
# define BUFFER_SIZE 32  /* fallback if not set via -D */
#endif
 
static char     *leftover;          /* persists between calls */
 
static char     *strjoin(const char *s1, const char *s2);
static char     *extract_line(char **leftover, char *nl);
static char     *flush_leftover(char **leftover);
 
char    *tci_getline(int fd)
{
    char    buf[BUFFER_SIZE + 1];   /* +1 for null terminator */
    char    *nl;
    char    *tmp;
    ssize_t bytes;
 
    if (!leftover)
        leftover = tci_strdup("");   /* initialise on first call */
    while (1) {
        nl = tci_strchr(leftover, '\n');
        if (nl)
            return (extract_line(&leftover, nl)); /* line ready — no read needed */
        bytes = read(fd, buf, BUFFER_SIZE);
        if (bytes <= 0)
            return (flush_leftover(&leftover));   /* EOF or error */
        buf[bytes] = '\0';          /* read() does not null-terminate */
        tmp = leftover;
        leftover = strjoin(leftover, buf);        /* accumulate new chunk */
        free(tmp);                  /* free old leftover after joining */
    }
}

#ifndef BUFFER_SIZE provides a fallback if the file is compiled without -D BUFFER_SIZE=N. The tester always passes an explicit value; the fallback is a safety net for manual compilation.

The forward declarations above tci_getline let the compiler see each helper's signature before the call sites inside tci_getline and extract_line. Without them the compiler encounters an unknown identifier and stops — the same constraint you saw with dispatch in c02/02.

Run the tester

Build and run:

make re
bash test.sh

The short file and long file suites should pass. Edge cases — empty file, no trailing newline, BUFFER_SIZE=1 — are covered on the next page.