Most of my work today is Laravel, TypeScript and AI agents. But the code that taught me the most was written in C, on a Linux box, during the ALX Software Engineering programme. Over roughly a year I wrote a printf clone, a Unix shell, sorting algorithms, binary trees and a small bytecode interpreter called Monty, mostly in pairs, always checked by an automated grader and a strict style linter.
I still think about those projects every week. Here is what stuck.
Everything is a loop that reads, decides and acts#
The core of our simple_shell is a function called hsh. Stripped down, it looks like this:
while (r != -1 && builtin_ret != -2)
{
clear_info(info);
if (interactive(info))
_puts("$ ");
r = get_input(info);
if (r != -1)
{
set_info(info, av);
builtin_ret = find_builtin(info);
if (builtin_ret == -1)
find_cmd(info);
}
free_info(info, 0);
}
Read a line. Parse it. Check whether it is a builtin (cd, exit, env, alias, history). If not, search PATH, fork, execve and wait. Clean up. Repeat.
When I started building tool-calling agents years later, I recognised the shape immediately. An agent loop is a REPL where the "user" is a model: send messages, look at the stop reason, run the tools it asked for, append the results, go again. The hard parts are the same as in a shell too: what happens on bad input, how you stop, and who cleans up after a failed step.
Memory is your problem, all of it#
In C nobody frees memory for you. Our shell had a free_info function that took a flag for "free everything, we are exiting" versus "free this command's allocations, keep the history". Getting that wrong meant Valgrind reports that were longer than the program.
That discipline changed how I write code in garbage-collected languages. I still ask, for every resource: who owns it, when does it end, and what happens if the owner crashes halfway. In a web app that question shows up as database transactions and queue job retries. In agent systems it shows up as task leases: if an agent claims work and then dies, something has to notice and release it. Same question, different runtime.
Error codes are an interface#
Monty, the bytecode interpreter, manages a stack (or queue) of integers with opcodes like push, pall, pint, pop, swap, add and rotl. The header file starts with a block of error constants:
#define ERR_BAD_INST 100 #define ERR_BAD_MALL 101 #define ERR_INVLD_PARM 102 #define ERR_PUSH_USG 201 #define ERR_PINT_USG 202 #define ERR_POP_USG 203 #define ERR_DIV_ZRO 208
The grader checked exact error messages, like L3: can't pint, stack empty, printed to stderr with a failing exit status. It felt pedantic at the time. It was actually my first lesson in designing errors for a reader who is not you.
The same idea is at the centre of how I design tools for agents now. A model reading Error: 500 learns nothing. A model reading L3: can't pint, stack empty knows the line, the operation and the reason, and can recover. Good error messages are part of the API, not an afterthought.
Dispatch tables beat long if/else chains#
Monty maps opcode strings to functions with a small table:
instruction_t ops[] = {
{"push", op_push}, {"pall", op_pall}, {"pint", op_pint},
{"pop", op_pop}, {"swap", op_swap}, {"add", op_add},
{"nop", op_nop}, {"sub", op_sub}, {"div", op_div},
{NULL, NULL}
};
Adding an opcode meant writing one function and one line in the table. The shell's builtins worked the same way. This is exactly how I register agent tools today: a map from tool name to a handler and a schema. The model picks the name, the dispatcher finds the function, and unknown names fail loudly with a clear message.
Small files, strict style#
ALX used the Betty style checker: no more than five functions per file, forty lines per function, documented with a comment block. Our shell ended up split into builtin.c, environ.c, getLine.c, history.c, parser.c, tokenizer.c, memory.c and so on. Monty has one file per opcode.
I complained about it then. Now I see it as training in cohesion. When every function has to fit on a screen, you are forced to name things and split responsibilities early. I still keep Laravel controllers thin and push logic into small, named classes for the same reason.
Pairs and graders taught me to work with others#
Most of these projects were done in pairs. printf was my first group project, and sorting_algorithms was a two-person effort too. We had to agree on file boundaries, header files and who owned which function before writing anything, because merging two people's C with a shared header is painful.
Years later I run several AI coding agents on the same repository, and the problem is the same. Two workers, one codebase, overlapping files. The solution is also the same: agree on boundaries first, isolate work, merge deliberately. That thinking eventually became worktree-orchestrator.
The automated grader mattered too. Every task had hidden checks, and you could not argue with them. That is the mindset I bring to agent evaluation: define what success looks like before the run, then let a machine check it.
From C to Python to back ends#
After the low-level track the programme moved to Python and the AirBnB clone series. The first version was a command interpreter built on Python's cmd module, with create, show, update and destroy commands over a JSON file store. Later versions added a MySQL storage engine with SQLAlchemy, a Flask API and a small front end.
That progression (a console, then storage, then an API, then a UI) is a good way to learn how web software is layered. I still build that way: data model and commands first, HTTP second, interface last.
The back-end specialisation covered caching strategies, Redis, pagination, user authentication with sessions and hashed passwords, and a files manager API in Node.js with MongoDB and a worker queue. None of it was glamorous. All of it shows up in production work.
What I would tell someone starting now#
- Write the boring version by hand once. Writing your own
getlineand_strtokmakes every future parser less mysterious. - Treat tooling as a teacher. Valgrind,
gcc -Wall -Werror -Wextra -pedanticand a style linter will teach you faster than any tutorial, if you stop fighting them. - Read your error messages as if you were a stranger. Future you, a teammate or a model will be that stranger.
- Learn to share a codebase. Group projects with clear ownership taught me more about software engineering than any single algorithm.
The repositories are all public on my GitHub: simple_shell, monty, printf, sorting_algorithms and binary_trees. They are student code, rough in places, and I keep them up on purpose. It is useful to be able to see where you started.