What is a Shell?#

A shell is a command-line interpreter — a program that reads text commands, interprets them, and executes them by calling the operating system’s kernel. It serves two roles: an interactive environment for running commands one at a time, and a programming language for automating sequences of commands.

flowchart LR
    User["User Input"] --> Shell["Shell (bash, zsh)"]
    Shell --> Kernel["OS Kernel"]
    Kernel --> HW["Hardware"]
    Kernel --> FS["File System"]
    Kernel --> Net["Network"]
    Shell --> Output["Output (stdout/stderr)"]

The shell is NOT the terminal. The terminal is the window application (iTerm2, GNOME Terminal). The shell is the program running inside it.


Shell Types#

ShellPathDescriptionUse Case
sh/bin/shPOSIX shell — minimal, portableSystem scripts, Docker
bash/bin/bashBourne Again Shell — most commonLinux scripting (default)
zsh/bin/zshZ Shell — extended featuresmacOS default, interactive use
dash/bin/dashDebian Almquist Shell — fastDebian/Ubuntu system scripts
fish/usr/bin/fishFriendly Interactive ShellInteractive use (not POSIX)

How to Check Your Shell#

echo $SHELL          # login shell
echo $0              # current shell
cat /etc/shells      # all available shells

Choosing a Shell for Scripts#

  • bash — default choice for scripts. Available everywhere on Linux, feature-rich
  • sh — when portability matters (Docker Alpine, BSD, embedded)
  • zsh — avoid for scripts (not guaranteed on servers)

Script Structure#

Every shell script follows this pattern:

#!/bin/bash
# Shebang — tells the OS which interpreter to use

# Description: What this script does
# Author: Your name

set -euo pipefail  # Strict mode (critical for reliability)

# Constants
readonly LOG_DIR="/var/log/myapp"

# Functions
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

# Main logic
log "Script started"
log "Script completed"

The Shebang Line#

The shebang (#!) must be the very first line — no blank lines or comments before it.

#!/bin/bash          # Use bash explicitly
#!/bin/sh            # POSIX shell (portable)
#!/usr/bin/env bash  # Find bash in PATH (more portable across systems)

#!/usr/bin/env bash is preferred when bash might be in different locations (e.g., /usr/local/bin/bash on macOS with Homebrew).


Running Scripts#

Three Ways to Execute#

# 1. Make executable and run directly
chmod +x script.sh
./script.sh

# 2. Run with interpreter explicitly (shebang ignored)
bash script.sh

# 3. Source — runs in CURRENT shell (variables persist)
source script.sh
. script.sh          # POSIX equivalent

Execute vs Source#

flowchart TD
    subgraph Execute["./script.sh (new process)"]
        E1["Parent shell"] --> E2["Fork child process"]
        E2 --> E3["Child runs script"]
        E3 --> E4["Child exits — variables lost"]
    end
    
    subgraph Source["source script.sh (same process)"]
        S1["Current shell"] --> S2["Runs commands in place"]
        S2 --> S3["Variables persist in current shell"]
    end

When to source:

  • Loading environment variables (.env files)
  • Loading shell functions for interactive use
  • Modifying the current shell’s state (cd, export)

When to execute:

  • Running automation scripts
  • Anything that should be isolated from your shell

Strict Mode: set -euo pipefail#

This single line prevents the majority of shell scripting bugs:

FlagEffectWithout It
-eExit on any command failureScript continues after errors
-uError on unset variablesUnset vars expand to empty string
-o pipefailPipe fails if any command failsOnly last command’s exit code matters

Why Each Matters#

# Without -e: script continues after failure
cp important.dat /backup/    # fails if /backup/ doesn't exist
rm important.dat             # deletes the only copy!

# Without -u: typos cause silent bugs
filename="report.txt"
rm "$filname"                # typo! expands to empty string

# Without pipefail: hidden failures in pipes
curl http://api.example.com | jq '.data'
# If curl fails, jq gets empty input, pipe "succeeds"

When to Temporarily Disable#

set -euo pipefail

# Some commands legitimately return non-zero
set +e
grep "pattern" file.txt   # returns 1 if no match
result=$?
set -e

# Or use || true
grep "pattern" file.txt || true

Exit Codes#

Every command returns an exit code (0-255):

CodeMeaning
0Success
1General error
2Misuse of shell command
126Command found but not executable
127Command not found
128+NKilled by signal N (e.g., 137 = SIGKILL)
# Check last exit code
ls /nonexistent
echo $?    # 2

# Use in conditionals
if command; then
    echo "Success"
else
    echo "Failed with code $?"
fi

# Set your own exit code
exit 0     # success
exit 1     # failure

Interactive vs Non-Interactive#

AspectInteractiveNon-Interactive (Script)
Reads fromKeyboardFile
Loads.bashrc, .bash_profileNothing (unless sourced)
PromptYes (PS1)No
Job controlYesLimited
PATHFull user PATHMay be minimal

This matters for cron jobs and CI/CD — your script won’t have the same PATH or aliases as your interactive shell.

# In scripts, always use full paths or set PATH explicitly
PATH="/usr/local/bin:/usr/bin:/bin"
export PATH

Key Takeaways#

  1. Always use a shebang — #!/bin/bash or #!/usr/bin/env bash
  2. Always use strict mode — set -euo pipefail prevents silent failures
  3. Execute for isolation, source for environment — know the difference
  4. Exit codes are your API — 0 means success, anything else is failure
  5. Scripts don’t inherit your interactive environment — set PATH explicitly in cron/CI
  6. Use bash for scripts unless you specifically need POSIX portability