The Unix Underneath: Terminal, zsh & the BSD Toolset

macOS

Chapter 4 · The Unix Underneath: Terminal, zsh & the BSD Toolset

Chapter 1 proved Darwin was real with a single command, and Chapter 3 mentioned Terminal as the escape hatch for whatever System Settings can't reach. This chapter finally opens Terminal properly and stays there — going hands-on with the default shell and the command-line toolset that actually implements the BSD heritage Chapter 1 only described. Cmd+Space, type Terminal, press Return, and everything below can be followed along directly.

zsh: The Default Shell Since Catalina

macOS Catalina (10.15, 2019) switched the default shell from bash to zsh (the "Z shell"). The reason is a genuinely specific one, not a stylistic preference: bash versions from 4.0 onward are licensed under the GPLv3, and Apple's legal position avoids shipping GPLv3 software in macOS. macOS still bundles bash today — but it's a frozen, ancient bash 3.2, the last release under the older GPLv2, kept only for backward compatibility with scripts that explicitly call it. zsh, by contrast, is BSD-licensed, which is exactly why it was free to become the new default instead.

zsh is close enough to bash that most everyday interactive use and simple scripts carry over directly — but Bash Scripting Fundamentals' own material doesn't automatically transfer without at least one real gotcha worth knowing up front:

# bash (and Bash Scripting Fundamentals' own examples): arrays are zero-indexed fruits=(apple banana cherry) echo "${fruits[0]}" # apple # zsh, macOS's default shell: arrays are ONE-indexed by default fruits=(apple banana cherry) echo "${fruits[0]}" # empty/error in default zsh — index 0 doesn't exist echo "${fruits[1]}" # apple — this is what bash would call index [0]

A bash script relying on zero-indexed arrays will silently misbehave if run under zsh without a shebang explicitly pinning it to #!/bin/bash — worth confirming which interpreter a script actually declares, rather than assuming Terminal's default shell is bash just because it always used to be.

The BSD Toolset: More Gotchas Than Just ls

Chapter 1 showed ls --sort=time failing on macOS because its BSD-derived ls never implemented that GNU long-option spelling. That was one example of a much wider pattern — the same command names, shipped by two different Unix lineages, quietly disagreeing on flags. Two more, both genuinely sharp enough to cause real damage if missed:

# GNU sed (most Linux distributions): -i takes an OPTIONAL backup suffix sed -i 's/foo/bar/' file.txt # edits in place, no backup — works fine # BSD sed (macOS): -i REQUIRES an explicit suffix argument, even if empty sed -i '' 's/foo/bar/' file.txt # correct on macOS — '' means "no backup" sed -i 's/foo/bar/' file.txt # WRONG on macOS: 's/foo/bar/' is consumed as # the backup suffix, and file.txt as the script — fails or silently misbehaves
# GNU date (Linux): -d parses flexible date strings directly date -d "next monday" # BSD date (macOS): no -d string parsing; -v adjusts by value instead, differently date -v+7d # seven days from now — a different flag, a different mental model

A third, quieter one: GNU's readlink -f (resolve a path to its final, absolute, symlink-free form) has no BSD equivalent on macOS at all by default — scripts relying on it need a workaround, or a substitute tool entirely.

The practical fix people actually reach for
Installing GNU's own coreutils via Homebrew (Chapter 5) puts GNU-flavored versions of these tools on the system alongside the BSD originals, prefixed with a ggsed, gdate, greadlink — so a script that genuinely needs GNU behavior can call the g-prefixed version explicitly, rather than fighting BSD's own flags or hoping they happen to match.
Copy-pasting a Linux one-liner into Terminal without checking is a real risk
The sed -i example above isn't a hypothetical — a tutorial or Stack Overflow answer written against GNU sed, pasted directly into macOS's Terminal, will either error out confusingly or, worse, silently do something other than what was intended, because BSD sed parses the exact same argument list differently. Any one-liner copied from a source that doesn't explicitly say "tested on macOS" is worth reading once before running, not just trusting because the command name matches.
The same lesson, now a third time on this site
Comparative Linux Distributions 5 first named this pattern comparing GNU-based distributions against BusyBox-based ones. Chapter 1 of this course showed it again, comparing macOS's BSD ls against GNU's. This chapter is the same finding a third time, with sed, date, and readlink as the evidence: whenever more than one Unix lineage is involved, matching command names are never a guarantee of matching command behavior. Three separate courses, three separate concrete examples, one single underlying rule.

Where This Course Is Headed

Homebrew as the community package-manager answer this chapter's own g-prefixed tools already previewed, the application model (app bundles, code signing, Gatekeeper), APFS, the built-in security stack, Time Machine and Recovery, networking and sharing, everyday troubleshooting tools, and a capstone setting up and securing a complete new Mac end to end.

Hands-On Exercises

Exercise 1

Explain the specific licensing reason Apple switched macOS's default shell from bash to zsh in Catalina, and what version of bash macOS still bundles today, and why that particular version was chosen rather than a newer one.

📄 View solution
Exercise 2

Walk through exactly what happens if you run sed -i 's/foo/bar/' file.txt on macOS's BSD sed, expecting GNU sed's behavior. Which argument gets misinterpreted as what, and how would you fix the command to behave correctly on macOS?

📄 View solution
Exercise 3

This chapter calls its own finding-box "the same lesson, now a third time on this site." Name all three courses/chapters involved and the specific command-line example each one used to demonstrate it.

📄 View solution

Chapter 4 Quick Reference

  • zsh — macOS's default shell since Catalina (2019); BSD-licensed, unlike GPLv3-licensed modern bash
  • bash 3.2 — the old, frozen version still bundled with macOS, kept only for backward compatibility
  • Array indexing gotcha — zsh is 1-indexed by default; bash (and Bash Scripting Fundamentals' own examples) is 0-indexed
  • sed -i — BSD requires an explicit suffix argument (-i '' for "no backup"); GNU's is optional
  • date — GNU's -d parses flexible strings; BSD's -v adjusts by value, a different flag entirely
  • readlink -f — no BSD equivalent by default; Homebrew's g-prefixed coreutils (greadlink, etc.) are the common fix
  • This chapter's own throughline: matching command names never guarantee matching command behavior across Unix lineages — now shown three times across this site
  • Next chapter: Package Management — Homebrew