Skip to content
Built-in Commands: cd, pwd, exit
step 1/5

Reading — step 1 of 5

Read

~3 min readBuilt-ins & Variables

Built-in Commands: cd, pwd, exit

Here's a question that unlocks how shells really work: why can't cd be a normal program? There's no /bin/cd worth speaking of — and once you see why, the whole "built-in" category stops being trivia and becomes an architectural necessity you're about to implement.

The fork wall

Chapter 2 will make this rigorous, but the shape matters now: a shell runs external programs by forking a child process and exec-ing the program inside it. Whatever that child does — including changing its own working directory — dies with it. The parent shell's directory never moved.

So imagine cd as an external program:

shell (in /home/you)
 └─ forks → child runs "cd /tmp" → child's cwd is now /tmp → child exits
shell (still in /home/you)        ← nothing happened

A working cd must change the shell's own working directory — it must run inside the shell process, as a function call, not a child. That's the definition of a built-in: a command the shell executes itself because its whole purpose is to mutate shell state. The same logic condemns exit (must terminate the shell, not a child) and export/unset (must edit the shell's environment). pwd could be external (it only reads state) — and shells build it in anyway, for speed and consistency.

The dispatch rule this implies sits at the top of your executor: check the command name against the built-in table first; only fork for the rest.

Implementing the three

cd — the real work, with real rules:

  • cd <path> — change directory; on failure (no such dir, not a dir, no permission) print an error naming the path and set a non-zero status. The shell survives — a failed cd never kills a shell.
  • cd with no argument — go to $HOME.
  • cd - — go to the previous directory (and print it — real shells do). Which means your shell tracks OLDPWD: every successful cd saves where you came from. Two variables, updated together, and suddenly cd - toggling between two directories — the feature everyone's fingers know — costs four lines.

pwd — print the current directory, newline-terminated. One call to the OS's getcwd, plus the discipline of not trusting a cached string.

exit — terminate the shell with an optional status: exit uses the last command's status; exit 2 uses 2. It must actually end the loop — the test where exit appears mid-script and later commands must NOT run is the one that catches "I printed exit but kept going."

Status codes are part of the contract

Every built-in returns a status like any command: 0 for success, non-zero for failure (cd /nope → 1). This wires built-ins into everything downstream — cd /nope && rm -rf * not running the rm is the safety property, and it only works if your cd reports honestly. (The bash course taught you to rely on this; now you're the one being relied on.)

Your exercise: Implement cd

The spec walks the cases: plain paths, failure handling, no-argument → HOME, cd - with OLDPWD tracking. Keep the state explicit — current directory, previous directory — and update both on every successful change only (a failed cd must not clobber OLDPWD; that's the sneaky test). None of it is algorithmically hard, and that's the lesson: built-ins are where a shell stops being a program-launcher and becomes a stateful environment. Small state, handled with total precision — you'll recognize the discipline from everything else in this course.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…