Guide Linux Beginner

Shell & Terminal

Terminal vs shell explained in plain English, Bash vs sh vs Zsh, how a command actually gets executed, environment variables, PATH, and which startup file loads when.

8 min read

You’ve typed commands into a terminal a thousand times. But have you ever stopped to ask what’s actually happening between you pressing Enter and something appearing on screen? Turns out there are a few distinct pieces working together, and once you see how they fit, a bunch of confusing “why doesn’t this work in cron” type problems suddenly make sense.

What Is a Terminal?

Terminal

A terminal is, at its core, just a window that displays text and captures your keystrokes. That’s genuinely it. Historically, a “terminal” was an actual physical device — a screen and keyboard wired up to a big mainframe computer somewhere else in the building. Today, when you open GNOME Terminal, iTerm2, or Windows Terminal, you’re running a terminal emulator: a program that pretends to be one of those old physical terminals, purely so it can plug into the rest of the Linux ecosystem which still expects one.

The terminal itself doesn’t understand your commands at all — it has no idea what ls or cd even mean. All it does is show you text and send your keystrokes onward. The actual “understanding what you typed” part is a completely different program’s job.

What Is a Shell?

Shell

That’s where the shell comes in. The shell is the actual program that reads what you typed, figures out what you meant, and makes it happen. When you type ls -la and hit Enter, it’s the shell that breaks that into pieces, works out that ls is a program, finds it, and runs it — then hands the terminal whatever output comes back so it can display it.

Common shells you’ll run into: bash, sh, zsh, and fish. They all do fundamentally the same job — reading and executing your commands — but with different features, syntax conveniences, and personalities, which we’ll get into shortly.

Terminal vs Shell — Seeing the Whole Chain

This is the bit that finally makes it click for most people: the terminal and the shell are two separate programs, connected through one more piece you rarely think about — the TTY (a kernel-level connector that plugs a terminal into whatever process is supposed to receive its input).

flowchart LR U[You Typing] --> T[Terminal Emulator] T --> TTY[TTY / PTY device] TTY --> S[Shell Process: bash] S --> C[Child Process: ls, grep, curl...]

So the real chain is: you type → the terminal captures it → the TTY passes it along → the shell interprets it → the shell launches whatever actual program you asked for (ls, grep, curl, anything). Four distinct layers, even though it feels instantaneous and seamless.

Bash, sh, and Zsh — Meet the Shells

Bash
Bash
sh
sh
Zsh
Zsh

Bash (Bourne Again SHell) is the one you’ll meet most often — it’s the default interactive shell on the vast majority of Linux distributions, and it’s a friendly superset of the original, older Unix shell with a ton of extra convenience features (arrays, [[ ]] tests, string manipulation, and more).

sh is the original, more minimal POSIX shell specification. On most modern Linux systems, /bin/sh is actually a symlink to a smaller, faster shell like dash rather than a truly separate program — it exists specifically for running simple, portable scripts that don’t need bash’s extra features. This matters more than people expect: if you write a script assuming bash-only syntax but it gets run with sh, it can fail in confusing ways.

Zsh has been the default shell on macOS since Catalina, and it’s popular with people who want a more powerful interactive experience — smarter tab completion, spelling correction, and a huge plugin ecosystem (Oh My Zsh being the famous one). For scripting purposes it’s very similar to bash, but the two aren’t 100% identical, so a script written for one occasionally needs small tweaks for the other.

echo $SHELL        # which shell is set as your default
bash --version        # confirm you're actually running bash
zsh --version            # or zsh

Shell Prompt

That $ or % symbol sitting there waiting for you to type something is called the prompt — and it’s completely customizable. By default it usually shows your username, hostname, and current directory, but people customize it constantly to show git branch info, exit codes, or anything else useful at a glance.

echo $PS1     # see your current prompt's configuration string
PS1="\u@\h:\w\$ "   # a typical prompt: user@host:directory$

Command Execution — What Actually Happens When You Hit Enter

This is genuinely worth slowing down for, because it explains SO many “why did that do that” moments. When you type ls -la | grep .conf > out.txt and press Enter, bash walks through these steps, in this exact order:

  1. Tokenization — splits your input into individual words/pieces.
  2. Expansion — resolves variables ($HOME), the tilde (~), brace patterns ({a,b}), and wildcards (*.txt) into their actual values.
  3. Command lookup — checks if it’s an alias, a shell built-in, a function, and only then searches $PATH for an actual program file.
  4. Redirection setup — wires up >, <, >>, 2>&1 before anything actually runs.
  5. Fork and execute — each stage of a pipeline becomes its own separate process, connected together by pipes.
  6. Wait — the shell waits for everything to finish and records the exit status in $?.
which python3       # where would the shell find this, if you ran it?
type ls                # is 'ls' an alias, a builtin, or an actual file?
echo $?                   # what was the exit status of the last command?

Shell Configuration

Shell Configuration

Your shell’s entire personality — its aliases, custom functions, prompt styling, and environment variables — comes from a handful of configuration files it reads when it starts up. This is genuinely one of the most practically useful things to understand, because it directly explains why “it works when I type it myself, but not when a script runs it” happens so often.

Environment Variables

Environment Variables

An environment variable is just a named value that’s available to a process and (if exported) any processes it launches — think of it as settings that travel with your session. $HOME tells programs where your home directory is, $USER holds your username, and $EDITOR tells tools like git commit which text editor to open.

env                    # list ALL environment variables currently set
echo $HOME                # look at one specific variable
export MY_VAR="hello"        # set a variable AND make it visible to child processes
unset MY_VAR                    # remove it

A variable that’s just assigned (MY_VAR="hello") stays private to your current shell. Only export-ed variables get passed down to programs and scripts that shell launches — this trips people up constantly.

PATH — How the Shell Actually Finds Programs

PATH

Ever wonder how typing python3 just… works, without you specifying where python3 actually lives on disk? That’s $PATH — a special environment variable holding a colon-separated list of directories the shell checks, in order, whenever you run a command by name.

echo $PATH
# /usr/local/bin:/usr/bin:/bin:/usr/local/games

which python3       # shows exactly which directory in PATH actually has it

If a program’s directory isn’t listed in $PATH, the shell simply won’t find it unless you type the full path yourself (/opt/myapp/bin/mytool). This is exactly why “command not found” errors happen right after installing something — the installer put the binary somewhere not yet in your $PATH.

export PATH="$PATH:/opt/myapp/bin"    # add a new directory to PATH for this session

Shell Startup Files — Which One Actually Runs?

Here’s the part that causes the most genuine confusion, so let’s be really precise about it. Bash loads different startup files depending on HOW it was launched:

Shell TypeWhen It HappensFiles Loaded
Login shellSSH-ing into a server, or a fresh terminal on some systems/etc/profile, then ~/.bash_profile (or ~/.profile)
Interactive, non-login shellOpening a new terminal tab/window on an already-logged-in desktop~/.bashrc
Non-interactive shellA script being run, or a cron job executingUsually none of the above, unless explicitly sourced
# Typical setup: .bash_profile just loads .bashrc, so you get consistent behavior either way
cat ~/.bash_profile
# if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

This is precisely why “I set an environment variable and it works in my terminal, but my cron job/CI pipeline can’t see it” is one of the most common Linux frustrations out there. Cron and most CI runners execute your script as a non-interactive, non-login shell — which means .bashrc never gets read at all, unless your script sources it explicitly.

Interactive vs Script Mode

#!/usr/bin/env bash
set -euo pipefail   # exit on error, undefined var, or failed pipe stage — standard for production scripts

VAR="value"
if [ -z "$VAR" ]; then
  echo "VAR is empty" >&2
  exit 1
fi
  • set -e: exit immediately if any command fails.
  • set -u: treat unset variables as an error.
  • set -o pipefail: a pipeline fails if any stage fails, not just the last one.

Production Considerations

  • Always use #!/usr/bin/env bash (not a hardcoded /bin/bash) so scripts work across systems where bash lives somewhere unexpected.
  • Quote your variables ("$VAR", not $VAR) — unquoted expansion is a top source of word-splitting and glob bugs in production scripts.
  • CI/CD runners often default to sh/dash, not bash — bash-only syntax ([[, arrays) will silently break there.
  • Never assume .bashrc ran before your script/cron job executed — set required environment variables explicitly within the script or its own config file instead.

Quick Interview Answer

“A terminal is just a window that displays text and captures keystrokes; the shell is the actual program — like bash — that interprets and executes what you type, connected to the terminal through a TTY. Bash is the common default, sh is the minimal POSIX shell used for portable scripts, and Zsh is macOS’s default with richer interactive features. Login shells load .bash_profile, interactive non-login shells load .bashrc, and non-interactive shells (cron, scripts) load neither by default — which is exactly why environment variables set in .bashrc often ‘disappear’ in automated jobs.”

Common Mistakes

  • Assuming .bashrc runs for every script or cron job — it usually doesn’t.
  • Writing bash-specific syntax ([[, arrays) in a script shebanged #!/bin/sh.
  • Forgetting that a variable needs export before a child process (or script) can actually see it.
  • Not quoting variables, leading to broken behavior on paths or values containing spaces.

Add More Questions to This Guide

Know a question that should be here? Share it and help the community!

Open Google Form