The normal method for reading a file in C is to define a buffer and load the file into an array. In this tutorial, we’re going to look at how to quickly load a file into a linked list so that we can handle files with an arbitrary amount of lines and unpredictable line length.

It’s been a couple years since I did any heavy lifting in C, so I decided to sit down in my spare time and really gain a master of it. To that end, I’m working on a small command-line application that searches through common log files for a given list of keywords. I’m no C guru, so I had a little fun figuring out how to handle a list of arbitrary length strings neatly. Here’s what I came up with.

Setup

Let’s get some necessary imports down:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "dbg.h" // Custom handlers for error checking

If you would like to use dbg.h, head over to the github for it. It cleans up a lot of boilerplate we need to handle memory in C. Otherwise, feel free to edit the following sections with your own safety-checking scheme.

Defining the Linked List

Next, we need to define the linked list. Nothing here should be a surprise.

typedef struct node {
	char *data;
	struct node *next;
} node;

And a single method to create a node:

struct node *create(char *data, node *next) {
	node *new_node = (node*) malloc(sizeof(node));
	check_mem(new_node); // from dbg.h
	new_node->data = data;
	new_node->next = next;
	return new_node;
error:
	printf("Encountered error allocated memory.");
	exit(0);
}

dbh.h verifies that we get a valid pointer, and redirects to the *error if we do not.

Reading the File

Now, we’re going to need a function to build the list.

node *get_lines(char *source) {
	FILE *fp;
	char *line = NULL;
	size_t len = 0;
	ssize_t read;
	struct node *head = NULL;
	
	// open the file	
	fp = fopen(source, "r");

	// verify that we opened the file
	check(fp != NULL, "Could not open file %s", source);

	// loop over the file and add a link for each line
	while ((read = getline(&line, &len, fp)) != -1) {
		head = create(strdup(line), head);
	}
	
	// clean up
	free(line);
	fclose(fp);
	return head;
error:
	exit(-1);
}

We get a small boon from the fact that we don’t care about the file order. This function builds the list backwards compared to the file, but that saves us a few lines of code and doesn’t affect my end case. If you need the list to be in order, you would need to accomplish this like so:

node *get_lines(char *source) {
	FILE *fp;
	char *line = NULL;
	size_t len = 0;
	ssize_t read;
	struct node *head, *cursor; // need two pointers

	head = cursor = NULL;
	
	fp = fopen(source, "r");
	check(fp != NULL, "Could not open file %s", source);
	
	while ((read = getline(&line, &len, fp)) != -1) {
		// if head is NULL, we're at the start of the list
		if (head == NULL) {
			head = cursor = create(&line, NULL);
		} else {
			// otherwise, create a node and join it to the end
			node *new_node = create(&line, NULL);
			cursor->next = new_node;
			cursor = new_node;
		}
	}

	free(line);
	fclose(fp);
	return head;
error:
	exit(-1);
}

Testing the Application

Let’s create some files and wrap this in a method to test it.

int main (int arc, char *argv[]) {
	node *files = get_files("~/.files");
	
	while(files != NULL) {
		printf("FILE: %s", files->data);
		files = files-> next;
	}
	
	return 0;
}

Then in the console:

$ echo "~/pg.log
> ~/server.log
> ~/traffic.log
> ~/other.log" > .files

My utility is called logfind, so then we have:

$ ./logfind .files
 STRING: ~/other.log
 STRING: ~/traffic.log
 STRING: ~/server.log
 STRING: ~/pg.log

So there you have it- how to read a file line by line into a linked list in C. I hope you found this article helpful. Let me know what you think in the comments.