tree prints the contents of a directory as an indented, ASCII-art hierarchy. It is one of the fastest ways to understand or document a project layout, and it ships as a small Homebrew package. This tutorial installs tree on macOS, walks through the flags you reach for most often, and wraps them in a reproducible demo project whose output is checked against committed golden files.
Prerequisites
- macOS with a Terminal (the built-in Terminal.app or iTerm2)
- Homebrew (installed in Step 1 if you do not have it)
make(pre-installed with the Xcode Command Line Tools on macOS)- No programming language or runtime required
The commands were validated with tree v2.3.2 and Homebrew 6.0.10 on macOS.
Step 1: Install Homebrew
Homebrew is the package manager used to install tree. Check whether it is already present:
brew --version
If that prints a version, skip ahead. Otherwise install Homebrew with the official one-liner from brew.sh, then confirm it works:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew --version
It is a good idea to refresh Homebrew’s package index before installing anything:
brew update
Step 2: Install tree
Install the package and confirm the binary is on your PATH:
brew install tree
tree --version
The version line looks like this:
tree v2.3.2 © 1996 - 2026 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro
If tree --version prints command not found, open a new Terminal tab so the shell picks up the freshly installed binary, or check that /opt/homebrew/bin is on your PATH.
Step 3: Run tree on the current folder
tree with no arguments prints the current directory and everything under it. Change into any project folder and run:
tree
On a deep folder that produces a lot of output, so the rest of this tutorial runs tree against a small, fixed sample directory instead. That keeps every shown output reproducible.
Step 4: Create a reproducible sample project
Set up a project folder to hold the demo. Create the ignore file first so generated output never gets committed by accident.
Create the file
mkdir -p tree-demo
cd tree-demo
touch .gitignore
Add the code: tree-demo/.gitignore
# Generated demo tree — rebuilt by `make sample`
sample/
# macOS / editor / log noise
.DS_Store
*.log
tmp/
Detailed breakdown
sample/is the folder a script regenerates on every run, so it is build output, not source. Ignoring it keeps the repository to the script plus the expected-output files..DS_Store,*.log, andtmp/cover the usual macOS and editor noise.- Creating
.gitignorebefore anything else means the very firstgit statusis already clean of throwaway artifacts.
Next, add a script that builds a known folder structure. A fixed structure is what makes tree’s output stable enough to test.
Create the file
mkdir -p scripts
touch scripts/make-sample.sh
chmod +x scripts/make-sample.sh
Add the code: tree-demo/scripts/make-sample.sh
#!/usr/bin/env bash
# Build a small, fixed folder tree under sample/ so `tree` output is reproducible.
# Rebuilds from scratch every run so the demo and tests are deterministic.
set -euo pipefail
root="${1:-sample}"
rm -rf "$root"
mkdir -p "$root/docs" "$root/src"
printf 'demo project\n' > "$root/README.md"
printf '# Guide\n' > "$root/docs/guide.md"
printf 'console.log("hi")\n' > "$root/src/app.js"
printf 'export const x = 1\n' > "$root/src/util.js"
# A dotfile so the `tree -a` (hidden files) demo has something to reveal.
printf 'local only\n' > "$root/.hidden-note"
echo "Built $root/"
Detailed breakdown
set -euo pipefailmakes the script fail on the first error, an unset variable, or a broken pipe, so a partial tree never masquerades as success.rm -rf "$root"clears any previous run before rebuilding, which is what guarantees byte-for-byte identical output each time.- The file bodies are written with
printfso their contents are fixed; the demo cares about the shape of the tree, not the file contents. .hidden-noteis a dotfile.treehides dotfiles by default, so it exists purely to demonstrate the-aflag in Step 6.
Run the script once to create the sample:
bash scripts/make-sample.sh
Built sample/
Step 5: View the tree, limit depth, and list only directories
With the sample in place, tree renders its structure. The --noreport flag drops the trailing N directories, M files summary line, which keeps the output stable when you later diff it in a test:
tree --noreport sample
sample
├── docs
│ └── guide.md
├── README.md
└── src
├── app.js
└── util.js
tree sorts entries case-insensitively, which is why docs comes before README.md. Two flags you will reach for constantly:
-L <n> limits how many levels deep tree descends. Depth 1 shows only the top level:
tree -L 1 sample
sample
├── docs
├── README.md
└── src
3 directories, 1 file
-d lists directories only and skips files entirely:
tree -d sample
sample
├── docs
└── src
3 directories
The count 3 directories includes the top-level sample folder itself.
Step 6: Reveal hidden files with -a
By default tree omits dotfiles such as .gitignore or a .git directory. The -a flag includes them, which is how you surface the .hidden-note file the sample script created:
tree --noreport -a sample
sample
├── .hidden-note
├── docs
│ └── guide.md
├── README.md
└── src
├── app.js
└── util.js
Step 7: Lock the output with a test
Capture the two views you care about as golden files, then a test can diff live output against them. Generate the baselines from the current, known-good output:
mkdir -p expected
tree --noreport sample > expected/tree.txt
tree --noreport -a sample > expected/tree-all.txt
These files are committed (they are the expected result), unlike the generated sample/ folder. Now add the test that rebuilds the sample and compares.
Create the file
mkdir -p tests
touch tests/test_tree.sh
chmod +x tests/test_tree.sh
Add the code: tree-demo/tests/test_tree.sh
#!/usr/bin/env bash
# Verify `tree` output against committed golden files, so the article's shown
# output stays honest across edits. Rebuilds the sample first for isolation.
set -euo pipefail
cd "$(dirname "$0")/.."
command -v tree >/dev/null 2>&1 || {
echo "FAIL: tree is not installed (run: brew install tree)" >&2
exit 1
}
bash scripts/make-sample.sh sample >/dev/null
fail=0
check() {
local label="$1" golden="$2"; shift 2
if diff -u "$golden" <("$@"); then
echo "ok: $label"
else
echo "FAIL: $label does not match $golden" >&2
fail=1
fi
}
check "visible tree" expected/tree.txt tree --noreport sample
check "tree with hidden -a" expected/tree-all.txt tree --noreport -a sample
if [ "$fail" -ne 0 ]; then
echo "TESTS FAILED" >&2
exit 1
fi
echo "ALL TESTS PASSED"
Detailed breakdown
cd "$(dirname "$0")/.."moves to the project root so the script works no matter where it is called from.- The
command -v treeguard turns a missing binary into a clear message instead of a confusing failure deeper in the script. check()runs a command through process substitution (<(...)) anddiff -ucompares its output against the golden file, printing a unified diff on any mismatch.- The
failflag lets both checks run before the script exits non-zero, so one run reports every difference rather than stopping at the first.
Run it:
bash tests/test_tree.sh
ok: visible tree
ok: tree with hidden -a
ALL TESTS PASSED
Step 8: Drive it all with a Makefile
A short Makefile gives the demo memorable entry points and a default help screen.
Create the file
touch Makefile
Add the code: tree-demo/Makefile
.DEFAULT_GOAL := help
SHELL := /bin/bash
.PHONY: help sample demo demo-all test clean
help: ## Show this help screen
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-10s %s\n", $$1, $$2}'
sample: ## Build the reproducible demo folder tree under sample/
@bash scripts/make-sample.sh sample
demo: sample ## Print the visible tree (no hidden files)
@tree --noreport sample
demo-all: sample ## Print the tree including hidden files (tree -a)
@tree --noreport -a sample
test: ## Rebuild the sample and diff tree output against expected/
@bash tests/test_tree.sh
clean: ## Remove the generated sample/ folder
@rm -rf sample
@echo "Removed sample/"
Detailed breakdown
.DEFAULT_GOAL := helpmakes a baremakeprint the help screen instead of running the first target, so the project is self-documenting.- The
helptarget greps its ownMakefilefor the##comment on each target line and formats them into an aligned list, which keeps the help text in sync with the targets automatically. demoanddemo-alldeclaresampleas a prerequisite, so they always rebuild the fixture before printing it.SHELL := /bin/bashpins the recipe shell so thepipefail-style scripting behaves the same on any machine.
Run make with no arguments to see the help screen:
make
Targets:
clean Remove the generated sample/ folder
demo-all Print the tree including hidden files (tree -a)
demo Print the visible tree (no hidden files)
help Show this help screen
sample Build the reproducible demo folder tree under sample/
test Rebuild the sample and diff tree output against expected/
Then make test rebuilds the sample and verifies both views:
make test
ok: visible tree
ok: tree with hidden -a
ALL TESTS PASSED
Troubleshooting
tree: command not foundright after install. Open a new Terminal tab or runhash -rso the shell re-scansPATH. Confirm the location withwhich tree(expect/opt/homebrew/bin/treeon Apple Silicon).make testreports a diff. Regenerate the goldens with the two commands in Step 7 if you changed the sample structure on purpose; otherwise the diff is pointing at a real mismatch.- A different
treeversion changes the summary line. The tests use--noreport, so the version-sensitiveN directories, M filesline is excluded and the structure comparison stays stable.
Recap
You installed tree with Homebrew, learned the flags that matter most (-L for depth, -d for directories only, -a for hidden files, --noreport for stable output), and built a Makefile-driven demo whose output is pinned by golden files and a test. To go further, look at -I '<pattern>' to prune noisy folders like node_modules, -h to show human-readable file sizes, and -o <file> to write the tree straight into documentation. Run tree --help for the full list.