Linux Fundamentals

The Terminal & Shell Basics

18 min Lesson 2 of 26

The Terminal & Shell Basics

Every server you will ever manage in production runs Linux and accepts commands through a terminal. There are no GUIs, no right-click menus, no drag-and-drop. The terminal is the interface — and mastery of it is the single most leveraged skill a DevOps engineer can develop. Before you navigate filesystems or write pipelines, you need to understand what you are actually talking to when you open that black window.

Terminal, Shell, Console — What Is the Difference?

These three words are used interchangeably in casual conversation but they mean distinct things:

  • Terminal (emulator): A program that draws a window, handles keyboard input, and renders text output. Examples: gnome-terminal, iTerm2, Windows Terminal, alacritty. On a remote server you connect via ssh, and the SSH session itself acts as a terminal.
  • Shell: The program that runs inside the terminal. It reads your commands, interprets them, and hands them to the kernel. The shell is the language interpreter — it has variables, loops, functions, and conditionals. Common shells: bash (Bourne Again Shell, the Linux default), zsh (macOS default since Catalina), sh (POSIX-compliant minimal shell), fish (friendly interactive shell), dash (ultra-lean, often /bin/sh on Ubuntu).
  • Console: Originally the physical text-mode screen and keyboard attached directly to a server. In modern usage it usually means the terminal you get when you log in locally, or a cloud provider's out-of-band serial console you use when SSH is down.
Key idea: When you write a shell script with #!/bin/bash or #!/bin/sh, you are choosing the interpreter — exactly like choosing Python 3 vs Python 2 with #!/usr/bin/env python3. The shebang line matters in production because dash (often /bin/sh on Ubuntu) does not support bash-specific syntax like [[ or local -n. Always use #!/usr/bin/env bash in scripts that need Bash features.

Reading the Shell Prompt

When you open a terminal, the shell displays a prompt — a short string that tells you where you are and who you are before waiting for input. Decoding it immediately saves confusion:

# Typical prompt formats you will see in the wild # Format 1 — default Bash on most Linux servers edrees@prod-web-01:~$ # Format 2 — root user (notice # instead of $) root@prod-db-01:/etc/nginx# # Format 3 — common on minimal Docker / CI containers bash-5.2$ # Anatomy of Format 1: # edrees = current username # prod-web-01 = hostname (critical — tells you WHICH server you are on) # ~ = current directory (~ is shorthand for your home directory) # $ = prompt character ($=normal user, #=root) # Always verify hostname when SSHing to multiple servers. # Killing the wrong database is a career-ending event. hostname # print just the hostname whoami # print current user pwd # print working directory (full absolute path)
Production pitfall: Senior engineers with root access to dozens of servers have destroyed production data by running destructive commands on the wrong host. Always verify hostname and whoami before running anything irreversible. Some teams color-code the shell prompt red for production hosts — a visual interrupt that forces a pause.

Anatomy of a Command

Every shell command follows a consistent structure. Understanding this structure lets you read any command you encounter, even one you have never seen before:

Anatomy of a Shell Command ls -lh --sort=size /var/log 2>&1 | head -20 Command Short Flags Long Option + Value Argument (Path) Redirection Pipe to Next Cmd command [short-flags] [--long-option[=value]] [arguments] [redirections] [| pipe-chain] Items in [ ] are optional. Order matters: flags before arguments. Multiple short flags can combine: -lha = -l -h -a
Full anatomy of a shell command: the command name, short flags, long options with values, path arguments, I/O redirections, and a pipe to a downstream command.

Breaking down the example: ls -lh --sort=size /var/log 2>&1 | head -20

  • ls — the command (list directory contents)
  • -lh — combined short flags: -l (long format) and -h (human-readable sizes)
  • --sort=size — long option with a value: sort by file size
  • /var/log — the argument: directory to list
  • 2>&1 — redirect stderr (file descriptor 2) into stdout (file descriptor 1) so errors are visible in the pipe
  • | head -20 — pipe output to head, showing only the first 20 lines

Getting Help: man, --help, and Beyond

At big-tech companies, engineers routinely work with commands they have not memorized. The ability to rapidly find authoritative answers is more valuable than memorizing flags. Linux provides several layers of documentation:

# Layer 1 — Quick flag reminder (almost universal) ls --help git commit --help # some tools open the full man page instead # Layer 2 — The manual (man pages) man ls # full manual for ls man 5 passwd # section 5 = file formats (vs section 1 = user commands) man man # the manual for the manual system itself # Inside man: navigate with arrow keys, search with /, quit with q # Jump to a specific section heading: /^EXAMPLES # Layer 3 — apropos: search man pages by keyword apropos "disk usage" # find commands related to disk usage man -k crontab # same thing with -k flag # Layer 4 — info pages (GNU tools have richer documentation here) info coreutils # full GNU coreutils documentation # Layer 5 — type / which / command: figure out WHAT a command actually is type ls # "ls is aliased to 'ls --color=auto'" or "ls is /bin/ls" type cd # "cd is a shell builtin" — no separate binary which python3 # full path to the binary that runs command -v git # POSIX-safe version of which (use in scripts) # Layer 6 — tldr: community-maintained practical examples (install separately) tldr curl # shows the 5 most common curl usage patterns
Pro practice: In shell scripts, always use command -v instead of which to check whether a program exists. which is not POSIX-standard and behaves inconsistently across distributions — on some systems it returns 0 even when the command is not found. The correct idiom is: if ! command -v docker >/dev/null 2>&1; then echo "docker not found"; exit 1; fi

Shell Types: Interactive vs Non-Interactive, Login vs Non-Login

This distinction catches every DevOps engineer at least once in production. A shell's behavior — which configuration files it reads — depends on how it was started:

  • Login shell: Started when you log in (SSH, su -, sudo -i). Reads /etc/profile, then ~/.bash_profile (or ~/.profile). Your PATH, JAVA_HOME, and other environment variables are loaded here.
  • Non-login interactive shell: A new terminal tab, or bash without -l. Reads ~/.bashrc. Aliases and prompt customizations live here.
  • Non-interactive shell: A shell script running in a CI pipeline, a cron job, or ssh host "command". Reads almost nothing — no .bashrc, no aliases. This is why a script that works locally fails in CI: /usr/local/bin is in your interactive PATH but not the script's.
# Diagnosing which config files your shell is reading # Print every file sourced during login (bash -x traces execution) bash -xl 2>&1 | grep "^\+" | head -30 # Check the current shell and its flags echo $0 # "-bash" = login shell; "bash" = non-login echo $- # flags: "i" present = interactive # The canonical fix for CI PATH problems: # In your script, explicitly source the env file, or hardcode full paths source /etc/profile.d/myapp.sh # source a specific env file /usr/local/bin/node --version # use absolute path instead of relying on PATH # In GitHub Actions / GitLab CI, always pin full paths or use # the setup-* actions that correctly export to GITHUB_PATH / $PATH

Shell History and Keyboard Shortcuts

Production speed comes from not retyping. These shortcuts are muscle memory for experienced engineers:

  • Ctrl+R — reverse search through history; type part of a command to find it
  • Ctrl+A / Ctrl+E — jump to beginning / end of line
  • Ctrl+W — delete one word backward
  • Ctrl+L — clear screen (same as clear)
  • !! — repeat the last command (classic use: sudo !! after forgetting sudo)
  • !$ — last argument of the previous command: mkdir /opt/myapp && cd !$
  • Alt+. — insert last argument of previous command (same as !$ but interactive)
  • Ctrl+C — send SIGINT to the foreground process (interrupt)
  • Ctrl+Z — suspend the foreground process (then fg to resume, bg to background)
  • Ctrl+D — send EOF; closes the current shell if at an empty prompt
Key idea: Shell history is persisted to ~/.bash_history (default 500–2000 lines). In production environments, HISTSIZE is often set to a much larger number (50,000+) and HISTTIMEFORMAT is configured so that every command is timestamped — critical for incident investigations that ask "what exactly did the engineer run and when?"

Understanding Shells at a Glance

How a Shell Command Reaches the Kernel You (Engineer) keystroke Terminal Emulator iTerm2 / gnome-terminal / SSH PTY / pipe Shell (bash / zsh / sh) Parse → Expand → Execute fork/exec External Binary /usr/bin/ls, git… Linux Kernel (syscalls)
The path a command travels: from your keystroke through the terminal emulator and shell interpreter, forking to an external binary if needed, down to kernel system calls.

What the Shell Does with Your Input

Understanding shell expansion order prevents subtle bugs in scripts and prevents accidental command injection in CI pipelines:

  1. Tokenization: Split the input on whitespace (unless quoted)
  2. Brace expansion: {a,b,c}a b c
  3. Tilde expansion: ~/home/edrees
  4. Parameter/variable expansion: $VAR → its value
  5. Command substitution: $(command) → its output
  6. Arithmetic expansion: $((1 + 2))3
  7. Word splitting: Split the result of expansion on IFS (default: space, tab, newline)
  8. Pathname expansion (globbing): *.log → matching filenames
  9. Quote removal: Remove the now-processed quotes
Pro practice: Always double-quote variable expansions in scripts: use "$VAR" not $VAR. Without quotes, a variable containing spaces splits into multiple arguments, breaking commands silently. This is the source of countless production script failures: rm -rf $DEPLOY_DIR with DEPLOY_DIR="" expands to rm -rf which removes the current directory tree.