Back to Blog

Your First Shell Script: Shebang, chmod +x & Running Scripts | Zsh Tutorial Lesson 6

Sandy LaneSandy Lane

Video: Your First Shell Script: Shebang, chmod +x & Running Scripts | Zsh Tutorial Lesson 6 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Zsh Lesson 6: Your First Shell Script — Shebang, chmod +x, Running

Save commands in a .sh file. First line: #!/usr/bin/env zsh (the shebang). chmod +x script.sh to 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 — finds zsh on 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).
  • +x to 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.