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, and tmp/ cover the usual macOS and editor noise.
  • Creating .gitignore before anything else means the very first git status is 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 pipefail makes 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 printf so their contents are fixed; the demo cares about the shape of the tree, not the file contents.
  • .hidden-note is a dotfile. tree hides dotfiles by default, so it exists purely to demonstrate the -a flag 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 tree guard turns a missing binary into a clear message instead of a confusing failure deeper in the script.
  • check() runs a command through process substitution (<(...)) and diff -u compares its output against the golden file, printing a unified diff on any mismatch.
  • The fail flag 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 := help makes a bare make print the help screen instead of running the first target, so the project is self-documenting.
  • The help target greps its own Makefile for the ## comment on each target line and formats them into an aligned list, which keeps the help text in sync with the targets automatically.
  • demo and demo-all declare sample as a prerequisite, so they always rebuild the fixture before printing it.
  • SHELL := /bin/bash pins the recipe shell so the pipefail-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 found right after install. Open a new Terminal tab or run hash -r so the shell re-scans PATH. Confirm the location with which tree (expect /opt/homebrew/bin/tree on Apple Silicon).
  • make test reports 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 tree version changes the summary line. The tests use --noreport, so the version-sensitive N directories, M files line 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.

References