For the final installment, we'll bundle every concept we've covered and build logscan, a console tool that reads and filters text logs. It's a small utility, but it touches file I/O, structs, pointers, function pointers, dynamic memory, and a Makefile.
What You're Building
You will create a small log analyzer that reads a file, parses each line into a structured record, and then either summarizes, filters, or tails the results. The goal is not just to finish one tool, but to see how earlier C concepts start working together inside a multi-file program.
New Terms in This Lesson
- Parser: Code that converts text input into structured data
- Filter: A function that keeps only the entries matching certain conditions
- Subcommand: Breaking commands into variants like
logscan statsandlogscan tail - Runner: A function that receives callbacks and executes tasks
Key Ideas
Study Notes
- Estimated time: 2–3 hours (design + implementation + testing)
- Prereqs: Experience with file I/O, structs, pointers, function pointers, and Makefiles
- Goal: Implement text parsing, dynamic arrays, callback-driven filters, and automated builds
logscan provides the following:
- Read a log file line by line and convert it into an array of
LogEntry statssubcommand that prints counts per level (INFO, WARN, ERROR)filter level=ERROR keyword=dbsyntax to combine conditions and output specific entriestail Nto show the last N entries
Each stage reinforces a concept.
The data flow looks like this:
input file -> read line by line -> store in LogEntry array -> branch by command (stats/filter/tail) -> print results
Project Structure
logscan/
├── include/
│ └── logscan.h
├── src/
│ ├── main.c
│ ├── parser.c
│ ├── commands.c
│ └── filters.c
├── sample.log
└── Makefile
sample.log example:
2026-03-18T09:00:01Z INFO auth login success user=alice
2026-03-18T09:01:15Z WARN db connection delay 120ms
2026-03-18T09:02:44Z ERROR payment card approval failed code=42
2026-03-18T09:03:11Z INFO auth token refreshed user=bob
Core Code Tour
include/logscan.h
#ifndef LOGSCAN_H
#define LOGSCAN_H
#include <stddef.h>
typedef enum {
LEVEL_INFO,
LEVEL_WARN,
LEVEL_ERROR,
LEVEL_UNKNOWN
} LogLevel;
typedef struct {
char timestamp[32];
LogLevel level;
char source[32];
char message[128];
} LogEntry;
typedef int (*entry_filter)(const LogEntry *entry, void *ctx);
typedef struct {
LogEntry *items;
size_t count;
} LogBuffer;
LogBuffer parse_file(const char *path);
void free_buffer(LogBuffer *buffer);
void run_stats(const LogBuffer *buffer);
void run_filter(const LogBuffer *buffer, entry_filter filter, void *ctx);
void run_tail(const LogBuffer *buffer, size_t n);
void run_filter_by_level(const LogBuffer *buffer, LogLevel level);
void run_filter_by_keyword(const LogBuffer *buffer, const char *keyword);
#endif
Parser (src/parser.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "logscan.h"
static LogLevel parse_level(const char *token) {
if (strcmp(token, "INFO") == 0) return LEVEL_INFO;
if (strcmp(token, "WARN") == 0) return LEVEL_WARN;
if (strcmp(token, "ERROR") == 0) return LEVEL_ERROR;
return LEVEL_UNKNOWN;
}
LogBuffer parse_file(const char *path) {
FILE *fp = fopen(path, "r");
LogBuffer buffer = {NULL, 0};
char line[256];
size_t capacity = 0;
if (!fp) {
perror("failed to open file");
return buffer;
}
while (fgets(line, sizeof(line), fp)) {
if (buffer.count == capacity) {
size_t new_cap = capacity == 0 ? 8 : capacity * 2;
LogEntry *new_items = realloc(buffer.items, new_cap * sizeof(LogEntry));
if (!new_items) {
perror("memory allocation failed");
free_buffer(&buffer);
break;
}
buffer.items = new_items;
capacity = new_cap;
}
LogEntry *entry = &buffer.items[buffer.count];
char level_str[8];
if (sscanf(line, "%31s %7s %31s %127[^\n]",
entry->timestamp, level_str, entry->source, entry->message) == 4) {
entry->level = parse_level(level_str);
buffer.count++;
}
}
fclose(fp);
return buffer;
}
void free_buffer(LogBuffer *buffer) {
free(buffer->items);
buffer->items = NULL;
buffer->count = 0;
}
Command Runners (src/commands.c)
#include <stdio.h>
#include "logscan.h"
static const char *level_to_string(LogLevel level) {
switch (level) {
case LEVEL_INFO: return "INFO";
case LEVEL_WARN: return "WARN";
case LEVEL_ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
void run_stats(const LogBuffer *buffer) {
size_t info = 0, warn = 0, error = 0;
for (size_t i = 0; i < buffer->count; i++) {
switch (buffer->items[i].level) {
case LEVEL_INFO: info++; break;
case LEVEL_WARN: warn++; break;
case LEVEL_ERROR: error++; break;
default: break;
}
}
printf("INFO=%zu WARN=%zu ERROR=%zu\n", info, warn, error);
}
void run_filter(const LogBuffer *buffer, entry_filter filter, void *ctx) {
for (size_t i = 0; i < buffer->count; i++) {
if (filter(&buffer->items[i], ctx)) {
printf("%s %s %-5s %s\n",
buffer->items[i].timestamp,
level_to_string(buffer->items[i].level),
buffer->items[i].source,
buffer->items[i].message);
}
}
}
void run_tail(const LogBuffer *buffer, size_t n) {
size_t start = buffer->count > n ? buffer->count - n : 0;
for (size_t i = start; i < buffer->count; i++) {
printf("%s %s %-5s %s\n",
buffer->items[i].timestamp,
level_to_string(buffer->items[i].level),
buffer->items[i].source,
buffer->items[i].message);
}
}
Filters (src/filters.c)
#include <string.h>
#include "logscan.h"
typedef struct {
LogLevel level;
} LevelCtx;
typedef struct {
const char *keyword;
} KeywordCtx;
int filter_by_level(const LogEntry *entry, void *ctx) {
LevelCtx *c = (LevelCtx *)ctx;
return entry->level == c->level;
}
int filter_by_keyword(const LogEntry *entry, void *ctx) {
KeywordCtx *c = (KeywordCtx *)ctx;
return strstr(entry->message, c->keyword) != NULL;
}
void run_filter_by_level(const LogBuffer *buffer, LogLevel level) {
LevelCtx ctx = {level};
run_filter(buffer, filter_by_level, &ctx);
}
void run_filter_by_keyword(const LogBuffer *buffer, const char *keyword) {
KeywordCtx ctx = {keyword};
run_filter(buffer, filter_by_keyword, &ctx);
}
Entry Point (src/main.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "logscan.h"
static LogLevel level_from_arg(const char *arg) {
if (strcmp(arg, "INFO") == 0) return LEVEL_INFO;
if (strcmp(arg, "WARN") == 0) return LEVEL_WARN;
if (strcmp(arg, "ERROR") == 0) return LEVEL_ERROR;
return LEVEL_UNKNOWN;
}
static void print_usage(void) {
printf("Usage:\n");
printf(" logscan stats <path>\n");
printf(" logscan filter level=<LEVEL>|keyword=<TEXT> <path>\n");
printf(" logscan tail <N> <path>\n");
}
int main(int argc, char *argv[]) {
if (argc < 3) {
print_usage();
return 1;
}
const char *command = argv[1];
if (strcmp(command, "stats") == 0) {
if (argc != 3) {
print_usage();
return 1;
}
LogBuffer buffer = parse_file(argv[2]);
run_stats(&buffer);
free_buffer(&buffer);
} else if (strcmp(command, "filter") == 0) {
if (argc != 4) {
print_usage();
return 1;
}
const char *arg = argv[2];
LogBuffer buffer = parse_file(argv[3]);
if (strncmp(arg, "level=", 6) == 0) {
run_filter_by_level(&buffer, level_from_arg(arg + 6));
} else if (strncmp(arg, "keyword=", 8) == 0) {
run_filter_by_keyword(&buffer, arg + 8);
} else {
print_usage();
}
free_buffer(&buffer);
} else if (strcmp(command, "tail") == 0) {
if (argc != 4) {
print_usage();
return 1;
}
char *end = NULL;
unsigned long parsed = strtoul(argv[2], &end, 10);
if (end == argv[2] || *end != '\0') {
fprintf(stderr, "tail count must be a positive integer.\n");
return 1;
}
LogBuffer buffer = parse_file(argv[3]);
size_t n = (size_t)parsed;
run_tail(&buffer, n);
free_buffer(&buffer);
} else {
print_usage();
return 1;
}
return 0;
}
Build Automation (Makefile)
CC = clang
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
BUILD = build
SRCS = src/main.c src/parser.c src/commands.c src/filters.c
OBJS = $(SRCS:src/%.c=$(BUILD)/%.o)
TARGET = $(BUILD)/logscan
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(BUILD)
$(CC) $(OBJS) -o $(TARGET)
$(BUILD)/%.o: src/%.c include/logscan.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
@mkdir -p $(BUILD)
run: $(TARGET)
./$(TARGET) stats sample.log
clean:
rm -rf $(BUILD)
Sample Run
$ make run
INFO=2 WARN=1 ERROR=1
$ ./build/logscan filter level=ERROR sample.log
2026-03-18T09:02:44Z ERROR payment card approval failed code=42
$ ./build/logscan filter keyword=token sample.log
2026-03-18T09:03:11Z INFO auth token refreshed user=bob
$ ./build/logscan tail 2 sample.log
2026-03-18T09:02:44Z ERROR payment card approval failed code=42
2026-03-18T09:03:11Z INFO auth token refreshed user=bob
Why It Matters
- Even small tools become maintainable when they have clear structure and automated builds.
- Parsing, filtering, and statistics live in separate files so responsibilities stay readable.
- The function-pointer +
void *context pattern shows up naturally in real utilities. - This capstone provides a base skeleton for future OS assignments or server tools.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요