Learning Session Summary

Bash & Zsh

Everything you learned — shells, scripts, variables, commands, and real-world use

Shell Basics

Bash
Bourne Again Shell. The classic Unix shell, default on most Linux systems. Almost every tutorial is written for Bash. Located at /bin/bash.
Zsh
Z Shell. Default on macOS since Catalina. Largely compatible with Bash — beginner scripts work identically in both. Located at /bin/zsh.
Key insight

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.

Terminal opens
Default: Zsh
Type bash
Nested Bash session
Type exit
Back to Zsh

Files & Permissions

The extension means nothing to the OS

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.

How ./script.sh decides which interpreter to use
Execute permission?
If no → "Permission denied". Fix with chmod u+x file.sh
Shebang present?
Yes → use that interpreter exactly (e.g. #!/bin/bash, #!/usr/bin/python3)
No shebang?
Falls back to current shell (Zsh on your Mac)
Running without execute permission

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.

Editors
  • touch file.sh — creates an empty file, no editor opens
  • vi file.sh — opens Vi: i to insert, Esc to stop, :wq save & quit, :q! quit without saving
  • nano file.sh — beginner-friendly, shows all commands at the bottom of the screen

Variables

Assignment rules
# Single word — quotes optional
country=Pakistan

# Value with spaces — quotes required
city='New York'
city="New York" # both work

# WRONG — Bash treats "Kingdom" as a command
country=United Kingdom # ❌
Single quotes ''
Everything is literal. No substitution happens. echo '$name' prints literally $name.
Double quotes ""
Variables and commands are substituted. echo "$name" prints the value of name.
Python parallel

Single quotes '' are like Python regular strings. Double quotes "" with $ substitution are like Python f-strings. All variables are strings by default — even numbers.

Arithmetic
i=42
echo $i + 1 # prints: 42 + 1 (literal!)
echo $((i + 1)) # prints: 43 ✓
No real types

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.

Plain string (default)
name="Vera"
number=42 # still stored as string "42"!
declare -i  · integer
declare -i count=5
count=count+1 # auto math → 6
count="hello" # assigns 0
Without -i you'd need $(( )) for math.
declare -r  · readonly
declare -r PI=3.14
PI=5 # error! cannot reassign
A constant — same as const in other languages.
declare -a  · array
declare -a fruits=("apple" "banana" "cherry")
echo ${fruits[0]} # apple (0-indexed)
echo ${fruits[@]} # all elements
echo ${#fruits[@]} # count
declare -A  · associative array
declare -A person
person[name]="Vera"
person[city]="Lisbon"
echo ${person[name]} # Vera
Closest Bash gets to a Python dict.
declare -x  · export
declare -x MY_VAR="hello"
# same as:
export MY_VAR="hello"
Makes the variable available to child processes.
Python → Bash cheat sheet
PythonBash equivalent
x = "hello"x="hello"
x = 42declare -i x=42
x = True / Falsex=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 = 5declare -r x=5
Key takeaways
  • Without declare, everything is a string.
  • Bash has no float type — use awk or bc for decimals.
  • Booleans don't exist — use strings "true"/"false" or 0/1.
  • declare attributes are optional — most scripts just use plain variables and $(( )) for math.

Command Substitution

Run a command, use its output inline
# Old style — backticks (legacy)
echo "Today is " `date`

# Modern style — preferred
echo "Today is $(date)"

Both produce identical output. Use $() — it's cleaner, easier to read, and can be nested easily.

Python parallel

$variable is like f"{variable}" — insert a value. $(command) is like f"{get_date()}" — call something and insert the result. The () signals "execute this".

Special variables
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

Operators & Conditions

[ ] — legacy
Old style. Crashes on empty variables. Use -a and -o for AND/OR. Avoid in new scripts.
[[ ]] — modern
Preferred. Handles empty variables safely. Supports &&, ||, == directly.
Numeric comparison operators
Bash Python Meaning
-eq == equal
-ne != not equal
-lt < less than
-le <= less than or equal
-gt > greater than
-ge >= greater than or equal
String tests
Operator Meaning
-z True if string is empty (zero length)
-n True if string is non-empty
== Strings are equal
!= Strings are not equal
# Check if a variable is empty
if [[ -z "$remote_head" ]]; then
  remote_is_empty=true
fi
Logical operators
# AND, OR, NOT inside [[ ]]
if [[ $age -ge 18 && $age -le 65 ]]; then echo "working age"; fi
if [[ $name == "John" || $name == "Jane" ]]; then ...; fi

# AND, OR outside conditions — command chaining
mkdir my_folder && cd my_folder # cd only if mkdir succeeded
cd my_folder || echo "folder not found"
if / fi structure
# ; before "then" because they're on the same line
if [[ $? -ne 0 ]]; then
  echo "Error occurred"
fi # "fi" = "if" backwards — closes the block

# "then" on its own line — no ; needed
if [[ $? -ne 0 ]]
then
  echo "Error occurred"
fi

Redirection & Pipes

Data flow symbols
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
Mental model

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.

Real example — error report script
# Find all errors, save to file
grep -i "error" /var/log/app.log > error_report.txt

# Count how many were found
echo "Found $(wc -l < error_report.txt) errors"

# Same thing in one line using pipe
grep -i "error" app.log | wc -l
Standard streams

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 — the black hole

/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.

Silencing output
# silence stdout only
command > /dev/null

# silence stderr only
command 2> /dev/null

# silence both (old style, portable)
command > /dev/null 2>&1

# silence both (modern Bash/Zsh shorthand)
command &> /dev/null
Real examples
# just check if the remote is reachable — ignore all output
git ls-remote git@github.com:VeraV/repo.git &> /dev/null

# only care whether grep found something (exit code)
grep "error" app.log > /dev/null

# hide "No such file" error message
ls fakefolder 2> /dev/null

# save stdout to file, silence errors
curl https://api.example.com > response.json 2> /dev/null

# combine with tee — log output and terminal, drop errors
command 2> /dev/null | tee output.log
Redirection cheat sheet
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)
Portability note

&> is a Bash/Zsh shorthand. For scripts that need to run across all POSIX shells, stick with the old-style > /dev/null 2>&1.

Exit Codes & Error Handling

$? = 0
Success. Every command that works returns 0. This is the only "happy" value.
$? ≠ 0
Something went wrong. Values like 1 (general error), 126 (permission denied), 127 (command not found).
set -euo pipefail — best practice header
#!/bin/bash
set -e # exit immediately on any error
set -u # exit if an undefined variable is used
set -o pipefail # catch errors inside pipes too

# Combined shorthand:
set -euo pipefail
Checking $? and bypassing set -e
# Save exit code immediately before it's overwritten
command1; status=$?
if [[ $status -ne 0 ]]; then echo "failed"; fi

# Concise alternative using ||
command1 || echo "command1 failed"

# Allow a command to fail without exiting (when using set -e)
grep "error" app.log || true

Essential Commands

grep — search text
grep -i "error" app.log # case-insensitive search
grep "error" app.log | wc -l # count matching lines
awk — extract columns from structured text
# $1, $2, $3... are columns (split by whitespace)
ps -ef | awk '{print $2}' # print all PIDs
awk -F',' '{print $1}' users.csv # comma-delimited CSV
awk '$2 > 25 {print $1}' users.txt # filter + print
df / | awk 'NR==2 {print $5}' # line 2, column 5
curl — transfer data to/from URLs
curl https://api.github.com/users/torvalds # GET request
curl -o file.zip https://example.com/file.zip # save to file
curl -X POST https://api.example.com/users \
     -H "Content-Type: application/json" \
     -d '{"name": "John"}' # POST with data
curl -s "https://wttr.in/Lisbon?format=%t" # silent, weather API

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.

cat, wc, find, sort, uniq
cat file.txt # display file contents
cat a.txt b.txt > combined.txt # combine two files
wc -l file.txt # count lines
find /var/logs -name "*.log" -mtime +30 -delete # delete old logs
cat app.log | grep "error" | sort | uniq # unique errors
less — pager for long output

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".

less file.txt # read a file
cat file.txt | less # pipe any output into less
man git | less # man uses less by default!
less — navigation keys
KeyAction
Space  /  fNext page
bPrevious page
j  /  ↓One line down
k  /  ↑One line up
gGo to top
GGo to bottom
/wordSearch for word
nNext search result
qQuit
How Git uses it

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.

less vs cat vs more
CommandBehavior
catDumps everything at once, no navigation
moreOld pager — forward only, can't go back
lessModern 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.

Cron Jobs

Schedule format
# ┌ minute (0-59)
# │ ┌ hour (0-23)
# │ │ ┌ day of month (1-31)
# │ │ │ ┌ month (1-12)
# │ │ │ │ ┌ day of week (0-7)
# │ │ │ │ │
* * * * * /bin/bash /path/to/script.sh
Common schedules
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.

Real-World Use Cases

🗄️ Automatic backup
cp -r ~/project ~/backups/project_$(date +%Y-%m-%d)
📊 Disk usage alert
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $USAGE -ge 90 ]]; then
  echo "Disk almost full!" | mail -s "Alert" you@email.com
fi
🚀 One-command deployment
git pull origin main \
  && npm install \
  && npm run build \
  && systemctl restart myapp \
  && echo "Deployed successfully!"
🔧 New machine setup
brew install git node python wget
git config --global user.name "Your Name"
git config --global user.email "you@email.com"
echo "Machine ready!"

Reserved Keywords

Shared Bash & Zsh keywords
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 (.)
Zsh-only
setopt, unsetopt, autoload, bindkey, zstyle, compdef, print — mostly for shell customisation, not scripting logic.
Notable difference
Array indexing: Bash starts at 0, Zsh starts at 1. Associative arrays: Bash uses declare -A, Zsh uses typeset -A.

The Golden Rule of Shell Scripts

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.

🔄 Parallels you discovered