Everything you learned — shells, scripts, variables, commands, and real-world use
/bin/bash.
/bin/zsh.
Both are installed simultaneously on your Mac. Zsh is your daily
driver, but Bash is always available at /bin/bash.
Confirm with which bash and which zsh.
bashexit
The .sh extension is a human convention — the OS
completely ignores it. A file called script.banana runs
the same as script.sh. What matters is the
shebang line and
execute permission.
./script.sh decides which interpreter to use
chmod u+x file.sh
#!/bin/bash, #!/usr/bin/python3)
When you call bash script.sh or
sh script.sh, you hand the file to the interpreter as a
text file to read. The file itself never needs execute permission —
the interpreter already has it. The shebang line is also ignored.
i to insert,
Esc to stop, :wq save & quit,
:q! quit without saving
echo '$name' prints literally $name.
echo "$name" prints the value of name.
Single quotes '' are like Python regular strings.
Double quotes "" with $ substitution are
like Python f-strings. All variables are strings by default — even
numbers.
Bash has no int, bool, float,
or list. Everything is fundamentally a string. Use
declare to give a variable an attribute so
Bash treats it specially.
-i you'd need $(( )) for math.
const in other languages.
dict.
| Python | Bash equivalent |
|---|---|
| x = "hello" | x="hello" |
| x = 42 | declare -i x=42 |
| x = True / False | x=true / x=false (just strings!) |
| x = [1, 2, 3] | declare -a x=(1 2 3) |
| x = {"a": 1} | declare -A x=([a]=1) |
| const x = 5 | declare -r x=5 |
declare, everything is a string.awk or bc for decimals."true"/"false" or 0/1.declare attributes are optional — most scripts just use plain variables and $(( )) for math.
Both produce identical output. Use $() — it's cleaner,
easier to read, and can be nested easily.
$variable is like f"{variable}" — insert a
value. $(command) is like
f"{get_date()}" — call something and insert the result.
The () signals "execute this".
| Variable | Meaning |
|---|---|
| $? | Exit code of the last command (0 = success, anything else = error) |
| $$ | PID of the current process |
| $0 | Name of the script |
| $1 … $9 | Positional arguments passed to the script |
| $# | Number of arguments |
| $@ | All arguments as separate words |
-a and
-o for AND/OR. Avoid in new scripts.
&&, ||,
== directly.
| Bash | Python | Meaning |
|---|---|---|
| -eq | == | equal |
| -ne | != | not equal |
| -lt | < | less than |
| -le | <= | less than or equal |
| -gt | > | greater than |
| -ge | >= | greater than or equal |
| Operator | Meaning |
|---|---|
| -z | True if string is empty (zero length) |
| -n | True if string is non-empty |
| == | Strings are equal |
| != | Strings are not equal |
| Symbol | Meaning | Example |
|---|---|---|
| > | Redirect output to file (overwrites) | ls > files.txt |
| >> | Redirect output to file (appends) | echo hi >> log.txt |
| < | Feed file as input to command | wc -l < file.txt |
| | | Pipe: output of one command into the next | cat log | grep error |
Think of > and < as arrows showing
the direction of data flow. The pipe | chains small,
focused tools together — the Unix philosophy of doing one thing
well.
Every Unix process has two output streams — normal output and error output travel on separate channels so you can redirect them independently.
| Stream | Number | Meaning |
|---|---|---|
| stdout | 1 | Normal output |
| stderr | 2 | Error output |
/dev/null is a special file that discards anything written to it. Think of it as a trash can that never fills up — useful when you only care about a command's exit code, not its output.
| Command | Effect |
|---|---|
> /dev/null |
Discard stdout |
2> /dev/null |
Discard stderr |
&> /dev/null |
Discard everything (Bash/Zsh) |
2>&1 |
Merge stderr into stdout |
> /dev/null 2>&1 |
Discard both (old style, portable) |
&> is a Bash/Zsh shorthand. For scripts that need to run across all POSIX shells, stick with the old-style > /dev/null 2>&1.
The \ at the end of a line is a line continuation
character — purely for readability. There must be nothing after it,
not even a space.
less is a built-in Unix pager — pre-installed on macOS
and Linux. It displays text one screenful at a time so long output
doesn't fly past you. The name is a joke: it's an improved
more, so "less is more".
| Key | Action |
|---|---|
| Space / f | Next page |
| b | Previous page |
| j / ↓ | One line down |
| k / ↑ | One line up |
| g | Go to top |
| G | Go to bottom |
| /word | Search for word |
| n | Next search result |
| q | Quit |
Git automatically pipes long output through less so you
can read it comfortably — that's why git log,
git diff, and git ls-remote drop you into
a scrollable view when the output is large.
| Command | Behavior |
|---|---|
| cat | Dumps everything at once, no navigation |
| more | Old pager — forward only, can't go back |
| less | Modern pager — full navigation and search |
less is strictly better than more for
reading — but in scripts you want cat behavior so
output flows through without interruption.
| Schedule | Meaning |
|---|---|
| * * * * * | Every minute |
| 0 0 * * * | Every day at midnight |
| 0 9 * * 1 | Every Monday at 9am |
| 0 */6 * * * | Every 6 hours |
Always use full paths in cron jobs — cron runs in a
minimal environment that may not find commands by short name. Use
/bin/bash /path/script.sh not just
script.sh.
| Category | Keywords |
|---|---|
| Conditionals | if, then, else, elif, fi |
| Loops | for, while, until, do, done |
| Case | case, in, esac |
| Functions | function, return, exit |
| Variables | export, local, readonly, unset |
| I/O | echo, printf, read |
| Script control | set, trap, exec, eval, source (.) |
setopt, unsetopt, autoload,
bindkey, zstyle, compdef,
print — mostly for shell customisation, not scripting
logic.
0, Zsh starts at
1. Associative arrays: Bash uses
declare -A, Zsh uses typeset -A.
If you find yourself typing the same sequence of commands more than a
few times — that's your signal to write a script. Automation is the
whole point. Start with set -euo pipefail, use
[[]] over [], always quote your variables,
and add a shebang so your scripts behave the same for everyone.
f"{var}" ≈
Bash "$var" — both substitute values
into strings
get_date() ≈
Bash $(date) — the
() signals "execute this"