← back to log
devlog · day 1

Building my own shell in Python — day 1

devlog · Aug 17, 2026

I want to master Python. Not "can write a script" — actually know it. And I've found that reading more tutorials stops paying off pretty quickly; past a certain point you're just watching someone else solve a problem you don't have.

So: what better way to learn a language than building the thing you already use every single day? I'm writing my own shell, from scratch, in Python.

It's a good target precisely because it isn't a toy. A shell has to deal with processes, the filesystem, text parsing, error handling, and input and output — the parts of a standard library you never touch in an exercise, but can't avoid here.

What a shell actually is

Strip away the decades of features and a shell is a loop. It prints a prompt, reads a line you typed, works out what you meant, does it, and comes back for the next one. That's it. Everything else is detail piled on top of those four steps.

The "works out what you meant" step is where it gets interesting, because a shell has two completely different ways to run something. Some commands are programs on disk — real files it goes and finds. Others are built into the shell itself, with no file anywhere.

Day 1: the loop and the builtins

Today I got the loop running and implemented the first set of commands: exit, echo, type, pwd, and cd — plus the ability to run external programs.

That last one means implementing the search itself: when you type a command that isn't a builtin, the shell walks through the directories listed in PATH, looking for a matching executable. Find it, run it. Don't find it, report that it doesn't exist. That lookup is something I've relied on thousands of times without ever once thinking about it.

type turned out to be the perfect command to write early, because its whole job is to expose that split:

$ type echo
echo is a shell builtin

$ type ls
ls is /usr/bin/ls

Writing it forces you to make the distinction explicit, in code, instead of leaving it as a vague sense that "some commands are special".

The bit that actually clicked

Here's the thing I understood properly for the first time today: cd cannot be an external program. Not "shouldn't be" — can't.

Every process has its own current working directory. When a shell runs an external program, that program starts as a separate child process with its own copy of that state. So if cd were a normal program on disk, it would launch, dutifully change its own working directory, then exit — and your shell, the parent, would be sitting exactly where it started. The command would do nothing, every time.

The only way it works is if the shell changes its own directory, in its own process. Which is why it must live inside the shell. Same logic for exit: a child process can't terminate its parent on the way out.

I'd read the phrase "shell builtin" a hundred times and treated it as trivia. It isn't trivia — it's a consequence of how processes work, and you feel it the moment you try to build one.

Where I am now

Currently working through the edge cases, and mainly: quoting.

Splitting a line on spaces works beautifully right up until someone types a space they meant to keep. echo "hello world" has to preserve those spaces exactly. Single quotes and double quotes don't behave identically. Escape characters let you smuggle a quote inside a quote. Suddenly the naive split is nowhere near enough, and what you actually need is a small parser that walks the line character by character, tracking what state it's in.

This is the part I expected to be a footnote and is turning out to be the real work. Which is usually a sign you're learning something.

More in day 2.

keep going

If tutorials have stopped teaching you anything, stop reading and rebuild something you use every day. You'll find out how much of it you never actually understood — and that the gap closes faster than you'd think once you're the one implementing it. The tools you take for granted are the best teachers you have.

← back to all notes