Bash
Bash is a program that reads lines of text and turns them into running processes. Learning it well means learning two things at once: a command language you type interactively, and a full programming language you write scripts in.
What a shell actually is
Section titled “What a shell actually is”A shell is a command interpreter. It sits between you and the operating system kernel and does four jobs, in this order, for every line you type:
- Read a line of text.
- Expand it — replace variables, run command substitutions, expand globs like
*.txtinto filenames, split the result into words. - Execute — find the program,
fork()a child process,exec()the program in it, and wait. - Report — collect the child’s exit status into
$?and print the prompt again.
The critical insight is step 2. The shell does a lot of text rewriting before any program runs.
grep foo *.log never reaches grep as three words — the shell expands *.log first, so grep
might actually receive twelve arguments. Nearly every confusing bash bug is a misunderstanding of
what happened during expansion.
The programs you run (ls, grep, curl) are separate executables on disk. The shell only
contributes the glue: expansion, redirection, pipes, control flow, and a handful of builtins
(cd, export, read) that must run inside the shell itself because they change the shell’s own
state.
sh vs bash vs zsh
Section titled “sh vs bash vs zsh”sh is a specification (POSIX), not a single program. Bash and zsh are implementations that add
their own extensions on top.
| What it is | Where you meet it | |
|---|---|---|
| sh | The POSIX shell standard. Minimal, portable, no arrays worth using, no [[ ]]. |
#!/bin/sh scripts, Docker RUN, system() calls. On Debian/Ubuntu /bin/sh is dash, a small fast POSIX shell — not bash. |
| bash | GNU Bourne-Again SHell. The de-facto Linux default and the subject of this section. Adds [[ ]], arrays, $(( )), process substitution, local. |
Default login shell on most Linux distros; /bin/bash. |
| zsh | A different superset with better interactive features (completion, globbing, prompts). Mostly bash-compatible for simple scripts, but not a drop-in: array indices start at 1, unquoted variables are not word-split. | Default interactive shell on macOS since Catalina. |
macOS ships /bin/bash version 3.2 (frozen in 2007 for licensing reasons). Anything from bash 4
onwards — associative arrays, ${var,,}, mapfile, &>> — is missing there. Install a modern bash
with Homebrew and use the env-style shebang so scripts pick it up.
Interactive shells vs scripts
Section titled “Interactive shells vs scripts”The same binary runs in two very different modes.
- Interactive: attached to a terminal, prints a prompt, has job control, expands aliases,
reads history. Started when you open a terminal, or with
bash -i. - Non-interactive: reads commands from a file or
-c, no prompt, aliases are off by default, job control is off. This is script mode.
Test which you are in — $- holds the current option flags, and interactive shells include i:
case "$-" in *i*) echo "interactive" ;; *) echo "script" ;;esacBash also distinguishes login shells (started at sign-in, or with -l) from non-login ones. This
determines which startup files run, which is why “my alias works in the terminal but not over SSH”
happens.
| Shell kind | Files read (in order) |
|---|---|
| Interactive login | /etc/profile, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile |
| Interactive non-login | /etc/bash.bashrc (Debian-family), then ~/.bashrc |
| Non-interactive (scripts) | Nothing — unless BASH_ENV is set to a filename |
Because scripts read no startup files, a script cannot rely on your aliases, functions, or
PATH tweaks. Everything a script needs must be defined in the script.
Your first script and the shebang
Section titled “Your first script and the shebang”#!/usr/bin/env bashset -euo pipefail
name="${1:-world}"printf 'Hello, %s!\n' "$name"The first line is the shebang (#!). It is not a comment to the shell — it is read by the
kernel. When you execute a file, the kernel looks at the first two bytes; if they are #!, it runs
the interpreter named on that line and passes the script’s path as an argument. So ./hello.sh bob
effectively becomes /usr/bin/env bash ./hello.sh bob.
Two common forms:
#!/bin/bash # exact path; fails where bash lives elsewhere (e.g. NixOS, some BSDs)#!/usr/bin/env bash # looks bash up in PATH; picks a newer bash on macOS/Homebrew#!/usr/bin/env bash is the recommended default. Its one trade-off: it obeys PATH, so it can pick
up an unexpected bash in a strange environment.
Other shebang facts worth knowing: Linux truncates the shebang line at 127 characters, and a file with no shebang that is executed directly is run by bash as a bash script (this is a bash courtesy, not a kernel rule — do not rely on it).
Making a script executable
Section titled “Making a script executable”File permissions decide whether ./hello.sh is allowed at all.
chmod +x hello.sh # add execute for user, group, other (masked by umask)chmod 755 hello.sh # explicit: rwx for owner, r-x for everyone elsels -l hello.sh# -rwxr-xr-x 1 you you 96 Jan 5 10:11 hello.shchmod u+x hello.sh restricts the execute bit to the owner only, which is the safer default for
scripts holding anything sensitive.
Three ways to run it
Section titled “Three ways to run it”bash hello.sh bob # 1. run with an explicit interpreter./hello.sh bob # 2. execute the file (needs +x and a shebang)source hello.sh bob # 3. read it into the *current* shell. hello.sh bob # same as source; `.` is the POSIX spelling| Method | New process? | Needs +x? |
Shebang used? | Sees/changes your shell’s variables |
|---|---|---|---|---|
bash script.sh |
Yes (child bash) | No | No — ignored, it is just a comment | No |
./script.sh |
Yes (child bash) | Yes | Yes | No |
source script.sh |
No | No | No | Yes |
The first two run the script in a child process. The child gets a copy of your exported
environment; anything it changes — the current directory, variables, PATH — dies with it. That is
why a script cannot change the directory of the shell that called it.
source is the opposite: bash reads the file’s lines as if you had typed them. Use it for things
that must affect the current shell — activating a virtualenv, loading ~/.bashrc, defining
functions.
# demo.sh contains: cd /tmp; export FOO=1./demo.sh ; pwd; echo "${FOO:-unset}" # => your original dir, "unset"source demo.sh ; pwd; echo "${FOO:-unset}" # => /tmp, "1"Note the exit status: a sourced file’s status is that of the last command it ran, and exit
inside a sourced file exits your shell. Use return in sourced files.
Getting help
Section titled “Getting help”Bash has more built-in documentation than most tools, and knowing which source answers which question saves a lot of guessing.
man bash # the complete reference (~5000 lines) — search inside it with /help # list all shell builtinshelp test # help for a *builtin*; man pages do not cover thesehelp -m read # same, formatted like a man pagetype -a echo # how bash would resolve this name — every match, in ordercommand -v ls # the single thing that would run (script-friendly, POSIX)ls --help # most GNU tools' own summaryapropos json # search man page descriptions by keywordinfo bash # the full GNU manual, more readable than man bashtype is the tool that answers “why did that command do something unexpected?” — it distinguishes
aliases, functions, builtins, keywords, and files:
type -t if # => keywordtype -t cd # => builtintype -a ls # => ls is aliased to `ls --color=auto' # ls is /usr/bin/lsWhere to go next
Section titled “Where to go next”- Syntax and variables — quoting, expansion, arrays. Read this first; most bugs live here.
- Control flow — exit codes,
if,case, loops, functions. - I/O, redirection and pipes — file descriptors, here-docs, process substitution.
- Text processing — grep, sed, awk, find, xargs and real pipelines.
- Scripting best practices —
set -euo pipefail,trap, argument parsing, ShellCheck. - Environment and jobs —
export,PATH, subshells, globbing, signals, job control.
Key points
Section titled “Key points”- The shell expands text before running anything; understanding expansion explains most surprises.
/bin/shis not bash. Choose your shebang deliberately:#!/usr/bin/env bashfor bash features.- Scripts run in a child process and read no startup files — they inherit only exported variables.
sourceruns code in your current shell;./script.shdoes not.help,type -a, andman bashanswer nearly every “what is this thing” question locally.