Shell Basics


  • Description: What a shell is, sh vs bash vs other shells, shebangs, ways to run a script, comments, exit codes, and the strict-mode flags worth turning on
  • My Notion Note ID: K2A-E-1
  • Created: 2020-06-03
  • Updated: 2026-05-18
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Shell, sh, and bash

  • Shell — program that reads a command language and runs commands. The user-facing interface to a Unix kernel.
  • Shell script — text file containing shell commands; .sh extension is convention, not required.
  • sh — historic Bourne shell. Today usually a POSIX-compliant minimum (often dash on Debian/Ubuntu, bash --posix on macOS).
  • bash — Bourne-Again SHell. GNU's drop-in replacement for sh with many extensions: arrays, [[ ]], parameter expansion, process substitution. Default interactive shell on most Linux.
  • Other shells: zsh (macOS default since 10.15), fish (interactive-focused, not POSIX), dash (small, fast, POSIX).
  • Pitfall: code that runs in bash may break under sh. [[ ]], arrays, function, <<<, $'...' are bash-only. If a script needs POSIX, use #!/bin/sh and stay in the subset.

2. Shebang Line

  • First line of a script: #! + interpreter path. Kernel reads it to pick the interpreter.
#!/bin/bash
#!/usr/bin/env bash    # portable: finds bash via PATH
#!/bin/sh              # POSIX-only subset
  • /usr/bin/env bash is the portable form — bash may live at /bin/bash (Linux), /usr/local/bin/bash (Homebrew macOS), etc. env looks it up via $PATH.
  • Without a shebang, the script is interpreted by the calling shell when run via ./script; if invoked as bash script, the shebang is ignored anyway.
  • Shebang flags work: #!/bin/bash -e enables errexit for the whole script. Some kernels parse only the first arg, so chained flags are unreliable — prefer set -e in the body.

3. Running a Script

bash script.sh        # explicit interpreter; shebang ignored
./script.sh           # uses shebang; needs +x permission
sh script.sh          # forces sh (POSIX) interpretation
source script.sh      # run IN CURRENT shell; var assignments persist
. script.sh           # POSIX synonym of source
  • ./script.sh vs bash script.sh — the first needs chmod +x script.sh and honors the shebang. The second doesn't.
  • source / . — runs the file as if its commands were typed into the current shell. Used to load env vars from ~/.bashrc or .env-style files. Without source, a child shell runs the script and any cd, export, variable changes vanish on exit.

4. Comments

# Single-line comment — from `#` to end of line.

echo hi  # also valid inline

: '
Multi-line "comment". : is the null command (does nothing, returns 0);
followed by a quoted string that : silently consumes.
'

: <<'EOF'
Heredoc-as-comment. Quote the delimiter (EOF in single quotes)
to disable expansion of $vars and `cmds` inside the block.
EOF
  • No real multi-line comment syntax. Two idiomatic workarounds: : '...' (single-quoted argument to the null command) and a quoted-delimiter heredoc fed to :.
  • Pitfall: unquoted heredoc delimiter expands $var, runs `cmd` — even inside a "comment". Always single-quote the delimiter when commenting.

5. Exit Codes

  • Every command returns an integer exit status in 0..255.
  • 0 → success; non-zero → failure. Specific codes are command-defined.
  • Available in $? immediately after the command:
ls /nonexistent
echo "exit=$?"        # exit=2 (or similar, depending on ls)
  • && / || short-circuit on exit code:
mkdir build && cd build           # cd only if mkdir succeeded
grep -q foo file || echo "missing"
  • Scripts return their last command's status by default. Set explicitly with exit n. By convention:
    • 0 success
    • 1 generic error
    • 2 misuse of shell builtin
    • 126 not executable
    • 127 command not found
    • 128+N killed by signal N (e.g., 130 = SIGINT, Ctrl-C)

6. Strict Mode (set -euo pipefail)

Default bash is permissive — undefined variables expand to empty, failed commands keep running, broken pipelines look fine. The "unofficial strict mode" turns that off.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
  • -e (errexit) — exit immediately if any command returns non-zero. Caveat: doesn't fire inside if, while, ||, && conditions; doesn't fire for the left side of a pipeline unless pipefail is also on.
  • -u (nounset) — treat unset variables as an error. echo "$undefined" now fails instead of printing nothing.
  • -o pipefail — a pipeline's exit code is the rightmost non-zero status, not just the last command's. Catches failures like false | true (which would otherwise look successful).
  • IFS=$'\n\t' — restrict word splitting to newlines and tabs, not spaces. Stops filenames-with-spaces bugs in unquoted expansions.
  • Pitfall: set -e is famously surprising. A let x=0 or ((x=0)) returns 1 (because the result is zero) and aborts the script. Use (( x=0 )) || true or x=$((0)) to dodge it.
  • Inverse flags exist: set +e re-enables permissive mode for a block, then set -e to restore.
  • Debug: set -x traces every command (prefixed with +) to stderr. set +x turns it off.

7. shellcheck

  • Static analyzer for shell scripts. Catches quoting bugs, dead code, POSIX/bash confusion, the 2>&1 > file ordering trap, and dozens of other gotchas.
  • Run: shellcheck script.sh — emits warnings with SC#### codes; each code is documented on the project's wiki.
  • Most editors have a shellcheck plugin for inline diagnostics.
  • Treat warnings as bugs by default; suppress with a directive only when intentional: # shellcheck disable=SC2086.

8. References