
How the shell works comes down to four steps it performs every time you press Enter: it splits your line into tokens, expands variables and patterns, finds the command, and then starts it as a new process. Before anything runs, the shell quietly rewrites what you typed, and that rewriting explains most of the behaviour that feels mysterious.
If you have ever had a command work in your terminal but fail in a script, or seen a file called my notes.txt treated as two files, you have met the shell's rules without knowing them. This article follows a single line of text from your keyboard to a running program, so those surprises become predictable.
- The shell is an ordinary program that reads commands and asks the OS to run them.
- Quotes, not spaces, decide where one word ends and the next begins.
- Variables, globs and ~ are expanded by the shell before the program starts.
- The shell looks for functions and builtins first, then searches PATH left to right.
- It forks a child, sets up redirections, then calls exec, so your shell survives every command.
- 0:00Intro
- 0:25Why the shell surprises you
- 1:12What a shell actually is
- 2:02Why a text interface still wins
- 2:49Step 1: splitting into tokens
- 3:39Step 2: expansion before running
- 4:33Step 3: finding the command
- 5:29Step 4: fork, redirect, exec
- 6:20One real command, walked through
- 7:09Where the shell shows up
- 7:49Common mistakes to avoid
- 8:44Everything in four steps
- 9:07Know the steps, trust the shell
What is a shell, exactly?
Strip away the dark window and blinking cursor, and a shell is simple: a program that reads your commands and asks the operating system to run them. It does not do the heavy work itself. It coordinates. You type a line, the shell works out what you mean, the OS kernel creates a process, and the program, say grep, runs and sends its output back to you.
The key insight is that the shell is just a program, an ordinary executable such as /bin/bash. The terminal window is a separate app that only displays the shell's input and output. Many shells exist, including bash, zsh, sh and fish. Their features differ, but the core steps described here are shared, so you learn them once and they apply everywhere.
- The shell reads and interprets; the kernel creates processes.
- The terminal displays; the shell decides.
- bash, zsh, sh and fish share the same core steps.
Why use a command line when GUIs exist?
Graphical interfaces cover nearly everything, so why does the shell survive? Because text is a universal interface. Any program, any file and any machine can be controlled through a line of text, even when no screen is attached at all.
Compare the two approaches. Clicking through a GUI means one action at a time, it is hard to repeat exactly, and it needs a screen and mouse. A typed command can be combined with others using pipes, saved as a script, and run over a remote link.
Two reasons stand out. Automation: anything you type once can go into a file, and the shell will repeat it identically as often as you like. Composability: instead of one giant app, you chain small tools that each do one job well. The shell is not old-fashioned. It is glue.
Step 1: How does the shell split a command into tokens?
Before anything else, the shell reads your line and splits it into words called tokens. This sounds trivial, but it is where many surprises begin, because the shell's idea of a word is not always the same as yours.
Take mv "a b.txt" docs. The quotes group a b.txt into a single token, giving three tokens: mv, "a b.txt" and docs. The quotes are then removed, and the program receives argv0 (mv), argv1 (a b.txt) and argv2 (docs). Without the quotes, a b.txt would become two tokens, and mv would try to move a file called a and another called b.txt.
Operators split tokens too. The pipe |, the redirect >, && and ; each end a token on their own, even when there are no spaces around them. The lesson of step one: quotes, not spaces, decide what a word is.
- Quotes group words into one token.
- Quotes are removed before the program sees the arguments.
- | > && ; are tokens even without surrounding spaces.
Step 2: What is shell expansion?
Once the line is split, the shell rewrites certain tokens before running anything. Three kinds of expansion matter most day to day: variables like $HOME, globs like *.txt, and the tilde ~, which stands for your home folder.
Follow echo ~/*.txt. Tokenizing gives echo and ~/*.txt. The tilde expands to /home/ana/*.txt. The glob then matches the files a.txt and b.txt. Finally echo runs and receives two arguments, the finished file names. Variables behave the same way: $USER is replaced with ana before echo starts, and an unset variable quietly becomes nothing at all.
The important consequence is that programs never see the star. The shell does the expansion, so echo only gets results. One edge case catches people out: if a glob matches nothing, bash passes the pattern on literally, so the program receives *.txt as plain text.
Step 3: How does the shell find a command on PATH?
With its final list of words, the shell must work out what the first word refers to. It checks in a fixed order: its own functions and builtins first, and only then the folders listed in your PATH variable. The type command shows you the answer, as in the session below.
cd has to be a builtin. A separate program would change directory inside its own process and then exit, leaving your shell exactly where it was. Only code running inside the shell can move the shell itself.
PATH order matters. Folders are searched left to right and the first match wins, so a grep in /usr/local/bin beats the one in /usr/bin. If there is no function, no builtin and no matching file on PATH, you get command not found. That explains the classic script failure: the script simply ran with a different PATH than your terminal.
- Order: functions, builtins, then PATH.
- PATH is searched left to right; first match wins.
- type and type -t reveal what a name resolves to.
type -t greet
type -t cd
type grep
echo $PATH
Step 4: What do fork, redirect and exec do?
Now the shell knows what to run, and here is the twist: it never replaces itself. It makes a copy. This classic Unix pattern is why your shell is still waiting for you after every command finishes.
First, fork creates a child process that is a copy of the shell. In the child, the shell redirects file descriptors, pointing output at a file such as out.txt or at a pipe. Then exec replaces the child with the program, for example grep. Meanwhile the parent shell calls wait and collects the result.
Pipes extend the same idea. For ls | wc -l, the shell forks two children and connects the output of ls to the input of wc, so both run at once. What wait brings back is the exit status: 0 means success, anything else means failure. Note that redirections are set up by the shell before the program even starts.
- fork: copy the shell into a child.
- redirect: wire up files and pipes in the child.
- exec: replace the child with the program.
- wait: the parent collects the exit status.
One real command, walked through
Here are all four steps on a command you might run while chasing a bug on a server: grep -i error "$LOG" > errors.txt. Assume LOG holds /var/log/app.log.
Bash splits the line into tokens, expands "$LOG" into the log path, and looks up grep on PATH. It then forks a child, points that child's standard output at errors.txt, and calls exec grep. The arguments grep receives are grep, -i, error and /var/log/app.log.
grep never sees the > or the dollar sign. It gets three arguments after its own name and writes to standard output, unaware that standard output is now a file. That clear division of labour, the shell preparing everything and the program just doing its job, is what makes the shell predictable.
Where does the shell show up?
Almost anywhere code runs without a screen. Scripts automate repeated jobs. SSH gives you a shell on a remote server. CI pipelines run each step as a shell command. Containers often start through one. And everyday development work lives there too.
The same four steps run whether you are typing by hand or an automated system is typing for you. That is why they are worth learning: a quoting bug you would shrug off on your laptop breaks a CI pipeline or a container start-up in exactly the same way, just with less helpful feedback.
- Scripts
- SSH servers
- CI pipelines
- Containers
- Day-to-day development
Common shell mistakes and how to avoid them
Almost every recurring shell bug traces back to steps one and two: how the line was split and what was expanded before the program ran. In the example below, file holds my notes.txt. Unquoted, rm $file is split after expansion, so rm receives two arguments. Quoted as "$file", it receives one.
The find line shows the opposite problem. The pattern is meant for find, not the shell, so single quotes stop the shell from expanding the star and find receives the pattern intact. Whenever a pattern belongs to the program, as with find -name, quote it so the shell leaves it alone.
Finally, check instead of guessing. type shows what a name resolves to, and set -x prints every command after expansion, so you see exactly what ran rather than what you assumed would run.
- Write "$var" unless you truly want it split, which is rare.
- Quote patterns meant for the program, like find -name.
- Use type and set -x to see what really runs.
file="my notes.txt"
rm $file # rm gets 2 args
rm "$file" # rm gets 1 arg
find . -name '*.txt' # find gets glob
Key takeaways
- The shell reads your line, then asks the OS to run it.
- Quotes decide the tokens, not spaces.
- Expansion happens before running, so programs only see results.
- Lookup goes functions, builtins, then PATH, left to right.
- Every command runs via fork, redirect, exec and wait.
- Quote your variables and know who expands what.
Frequently asked questions
Is the terminal the same thing as the shell?
No. The terminal is a separate app that displays input and output. The shell, such as /bin/bash, is the program that actually reads and interprets your commands.
Why does a command work in my terminal but not in my script?
Often the script runs with a different PATH, so the shell searches different folders and reports command not found. Quoting differences are the other common cause, since unquoted variables get split into several words.
Why is cd a builtin and not a separate program?
A separate program would change directory only inside its own process and then exit, leaving your shell where it was. To change the shell's own folder, cd must run inside the shell.
Does grep or ls see the * in my command?
Usually not. The shell expands the glob into matching file names before the program starts. In bash, if nothing matches, the pattern is passed on literally.
When should I quote a variable in the shell?
Almost always. Writing "$var" keeps the value as one argument, even if it contains spaces. Leave it unquoted only when you deliberately want it split into several words.
How can I see what the shell actually ran?
Use type to see whether a name is a function, builtin or file on PATH. Use set -x to print each command after expansion, just before it runs.