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
- 2. Shebang Line
- 3. Running a Script
- 4. Comments
- 5. Exit Codes
- 6. Strict Mode (
set -euo pipefail) - 7.
shellcheck - 8. References
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;
.shextension is convention, not required. - sh — historic Bourne shell. Today usually a POSIX-compliant minimum (often
dashon Debian/Ubuntu,bash --posixon 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/shand 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 bashis the portable form —bashmay live at/bin/bash(Linux),/usr/local/bin/bash(Homebrew macOS), etc.envlooks it up via$PATH.- Without a shebang, the script is interpreted by the calling shell when run via
./script; if invoked asbash script, the shebang is ignored anyway. - Shebang flags work:
#!/bin/bash -eenableserrexitfor the whole script. Some kernels parse only the first arg, so chained flags are unreliable — preferset -ein 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.shvsbash script.sh— the first needschmod +x script.shand 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~/.bashrcor.env-style files. Withoutsource, a child shell runs the script and anycd,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:0success1generic error2misuse of shell builtin126not executable127command not found128+Nkilled 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 insideif,while,||,&&conditions; doesn't fire for the left side of a pipeline unlesspipefailis 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 likefalse | 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 -eis famously surprising. Alet x=0or((x=0))returns 1 (because the result is zero) and aborts the script. Use(( x=0 )) || trueorx=$((0))to dodge it. - Inverse flags exist:
set +ere-enables permissive mode for a block, thenset -eto restore. - Debug:
set -xtraces every command (prefixed with+) to stderr.set +xturns it off.
7. shellcheck
- Static analyzer for shell scripts. Catches quoting bugs, dead code, POSIX/bash confusion, the
2>&1 > fileordering trap, and dozens of other gotchas. - Run:
shellcheck script.sh— emits warnings withSC####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.