Your First Shell Script: Shebang, chmod +x & Running Scripts | Zsh Tutorial Lesson 6
Video: Your First Shell Script: Shebang, chmod +x & Running Scripts | Zsh Tutorial Lesson 6 by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 6: Your First Shell Script — Shebang, chmod +x, Running
Save commands in a
.shfile. First line:#!/usr/bin/env zsh(the shebang).chmod +x script.shto make executable. Run with./script.sh. Why./? Because.is not on$PATH.
A shell script is just a file containing commands. Same commands you'd type at the prompt — Zsh runs them top to bottom.
Create the file
touch hello.sh
Open it in your editor (nano, vim, VS Code, anything). Add:
#!/usr/bin/env zsh
# My first script
echo "Hello from a script!"
date
echo "Done."
Save and quit.
The shebang
#!/usr/bin/env zsh
The first line of a script. Tells the OS which interpreter to use.
#!— magic bytes the kernel checks./usr/bin/env zsh— findszshon the user's$PATH.
You'll see other shebangs:
#!/bin/zsh # absolute path; only works if zsh is at /bin/zsh
#!/bin/bash # for Bash scripts
#!/usr/bin/env bash # portable form for Bash
#!/usr/bin/env python3
/usr/bin/env <interpreter> is the portable form — works wherever <interpreter> is on PATH. Always prefer it over hardcoded paths.
chmod +x: make it executable
./hello.sh
# zsh: permission denied: ./hello.sh
ls -la hello.sh
# -rw-r--r-- 1 alice staff 56 May 8 10:00 hello.sh
Default permissions for a created file: rw-r--r--. No execute bit, so the OS refuses to run it.
chmod +x hello.sh
ls -la hello.sh
# -rwxr-xr-x 1 alice staff 56 May 8 10:00 hello.sh
Now x is set for owner/group/others. Run:
./hello.sh
# Hello from a script!
# Wed May 8 10:01:23 PST 2026
# Done.
Permission flags:
+x— add execute (for everyone).+xto specific scopes:chmod u+x file(user only),chmod g+x(group),chmod o+x(others).-x— remove execute.- Numeric:
chmod 755 file(owner rwx, group rx, others rx).
For scripts you'll run yourself, chmod +x file is fine.
Why ./?
hello.sh
# zsh: command not found: hello.sh
./hello.sh
# (works)
The shell searches $PATH for commands. Your current directory isn't on $PATH (intentionally — security). So bare hello.sh isn't found.
./hello.sh is "the file hello.sh in the current directory" — explicit path, no PATH lookup.
To skip the chmod and shebang, you can also run:
zsh hello.sh
This invokes Zsh directly, passing hello.sh as the script to run. The file doesn't need +x or a shebang — but you have to remember to type zsh every time.
Putting scripts on PATH
For scripts you use often:
mkdir -p ~/bin
mv hello.sh ~/bin/hello # rename, no .sh
chmod +x ~/bin/hello
Add ~/bin to $PATH in ~/.zshrc:
export PATH="$HOME/bin:$PATH"
Reload:
source ~/.zshrc
Now hello runs from anywhere — no ./, no .sh.
This is how Unix tools are organized: small executable files on PATH, no extension.
A well-commented script
#!/usr/bin/env zsh
# greet.sh — print a personalized greeting
# Usage: ./greet.sh
# Constants
NAME="Alice"
LANG="English"
# Greeting based on language
if [[ "$LANG" == "English" ]]; then
echo "Hello, $NAME!"
elif [[ "$LANG" == "Spanish" ]]; then
echo "¡Hola, $NAME!"
else
echo "Hi, $NAME!"
fi
echo "Today is $(date +%A)."
Comments start with #. Use them to explain why, not what the code does.
Running options
# 1. Direct (with shebang + chmod +x)
./hello.sh
# 2. Explicit interpreter (no shebang or chmod needed)
zsh hello.sh
# 3. Source — runs in CURRENT shell (variables persist!)
source hello.sh
. hello.sh # same thing; "." is the POSIX form
# 4. From PATH (after putting in ~/bin)
hello
source is special — it doesn't fork a new shell. Variables and aliases set in the script persist in your current session. Useful for re-loading .zshrc.
Exit codes
Scripts return a numeric exit code: 0 for success, anything else for error.
#!/usr/bin/env zsh
if [[ ! -f input.txt ]]; then
echo "Error: input.txt not found" >&2
exit 1
fi
echo "Processing input.txt..."
exit 0
After running, check $?:
./script.sh
echo $?
# 0 (or whatever the script returned)
Exit codes are how shells chain commands:
./script1.sh && ./script2.sh # script2 only if script1 succeeded
./script1.sh || echo "FAILED" # error message only if script1 failed
By convention: - 0 — success. - 1 — generic error. - 2 — misuse (bad arguments). - 64-78 — sysexits.h codes. - 126 — not executable. - 127 — command not found.
Strict mode
For real scripts, start with strict-mode flags:
#!/usr/bin/env zsh
set -euo pipefail
# -e : exit on first error
# -u : error on undefined variables
# -o pipefail : pipe failures propagate
This catches typos and silent errors. Highly recommended.
A practical script
#!/usr/bin/env zsh
set -euo pipefail
# backup.sh — copy ~/Documents to /tmp/backup with timestamp
SRC="$HOME/Documents"
DEST="/tmp/backup-$(date +%Y%m%d-%H%M%S)"
if [[ ! -d "$SRC" ]]; then
echo "Error: $SRC not found" >&2
exit 1
fi
mkdir -p "$DEST"
cp -a "$SRC"/* "$DEST/"
echo "Backed up to $DEST"
echo "Files: $(find "$DEST" -type f | wc -l)"
Save as ~/bin/backup, chmod +x ~/bin/backup, and backup runs anywhere.
Common stumbles
Permission denied. Forgot chmod +x. chmod +x script.sh.
bash instead of zsh. macOS terminal might be bash if you have an old account. Check with echo $SHELL. Switch with chsh -s /bin/zsh.
Bare command not found. Forgot ./. The current dir isn't on PATH.
source vs ./. source script.sh runs in current shell — variables/aliases persist. ./script.sh forks a subshell — changes don't persist.
Shebang wrong. #!bin/zsh (missing slash) → "no such file." Always #!/usr/bin/env zsh or #!/bin/zsh.
exit confusion. exit in a sourced script exits your terminal. return is for inside functions. For sourced scripts, use return or just don't exit.
Spaces in filenames. chmod +x My Script.sh errors. Quote: chmod +x "My Script.sh". Or rename to avoid.
Old DOS line endings. Editing on Windows produces \r\n. Bash/Zsh chokes: bad interpreter. Fix: dos2unix script.sh or sed -i '' 's/\r$//' script.sh.
What's next
Lesson 7: variables and arithmetic. Declaring, expanding, environment, scope.
Recap
Save commands in a .sh file. First line: #!/usr/bin/env zsh (shebang). chmod +x script.sh makes it executable. Run with ./script.sh (current dir not on PATH). For tools, put in ~/bin/ and add to $PATH. source script.sh runs in current shell (vars persist); ./ forks. Exit codes: 0 success, non-zero error. Use set -euo pipefail in real scripts.
Next lesson: variables and arithmetic.