Pattern Rules and Wildcards
Summary: in this tutorial, you will learn learn pattern rules, wildcard functions, substitution references, vpath, and automatic dependency generation — the tools that make makefiles scalable.
Pattern Rules and Wildcards
In the previous tutorial, you learned about variables and automatic variables. But you still had to write a separate rule for every single .c file. Pattern rules fix that by letting you write one rule that handles an entire category of files.
What you'll learn in this tutorial:
- How pattern rules (
%.o: %.c) eliminate repetitive rules - How to use
$(wildcard ...)to find files automatically - How substitution references like
$(SRCS:.c=.o)transform file lists - How
VPATHandvpathhelp Make find files in other directories - How to generate header dependencies automatically (so changing a
.hfile triggers rebuilds)
The Problem: Repetition
Look at this Makefile for a project with five source files:
CC := gcc
CFLAGS := -Wall
main.o: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
utils.o: utils.c
$(CC) $(CFLAGS) -c utils.c -o utils.o
config.o: config.c
$(CC) $(CFLAGS) -c config.c -o config.o
network.o: network.c
$(CC) $(CFLAGS) -c network.c -o network.o
parser.o: parser.c
$(CC) $(CFLAGS) -c parser.c -o parser.oAll five rules do the same thing — compile a .c file into a .o file. The only difference is the file name. Pattern rules let you replace all of them with a single rule.
Pattern Rules
A pattern rule uses % as a wildcard that matches any non-empty string:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@This single rule means: "To build any .o file, look for a .c file with the same name, then run this command."
Here's how Make processes it:
- Make needs
main.o - It matches the pattern
%.o, so%=main - The prerequisite becomes
main.c(replacing%withmain) $<=main.c,$@=main.o- Runs:
gcc -Wall -c main.c -o main.o
The same rule handles every .c → .o compilation automatically.
The $* variable contains the text that % matched. In the example above, $* would be main. You rarely need $* directly, but it's good to know it exists.
Complete Example with Pattern Rules
CC := gcc
CFLAGS := -Wall -Wextra
TARGET := myapp
SRCS := main.c utils.c config.c network.c parser.c
OBJS := $(SRCS:.c=.o)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
# This ONE rule replaces five separate rules:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) $(OBJS)The wildcard Function
Instead of listing every source file by hand, you can ask Make to find them:
SRCS := $(wildcard *.c)This finds all .c files in the current directory and puts them in SRCS. If your directory contains main.c, utils.c, and config.c, then SRCS becomes main.c utils.c config.c.
Why is $(wildcard) needed instead of just using *.c? In a Makefile, *.c is not automatically expanded by Make (unlike in your terminal). If you write SRCS := *.c, the variable will literally contain the text *.c. The $(wildcard ...) function tells Make to expand it.
Finding Files in Subdirectories
# All .c files in the src/ directory
SRCS := $(wildcard src/*.c)
# All .c files in src/ and its subdirectories (one level deep)
SRCS := $(wildcard src/*.c src/**/*.c)Note: Make's wildcard function does not support recursive globbing (** for all levels deep) in all versions. For deep directory trees, you may need the $(shell find ...) approach, which we'll see later.
Substitution References
You already saw this briefly — substitution references let you transform a list of file names by replacing one pattern with another:
SRCS := main.c utils.c config.c
OBJS := $(SRCS:.c=.o)
# OBJS is now: main.o utils.o config.oThe syntax $(VAR:old=new) replaces old at the end of each word in VAR with new.
More examples:
SRCS := src/main.c src/utils.c
OBJS := $(SRCS:.c=.o)
# OBJS: src/main.o src/utils.o
# You can chain substitutions using patsubst (a function):
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
# OBJS: build/main.o build/utils.opatsubst vs substitution references:
$(SRCS:.c=.o)— simple, replaces suffix only$(patsubst src/%.c, build/%.o, $(SRCS))— more powerful, can change directory paths and use%as a wildcard
Both do the same basic job. Use substitution references for simple suffix changes and patsubst when you need to transform the path.
Building Into a Separate Directory
It's common practice to put compiled files in a build/ directory to keep your source tree clean:
CC := gcc
CFLAGS := -Wall -Wextra
TARGET := build/myapp
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | build
$(CC) $(CFLAGS) $^ -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
build:
mkdir -p build
clean:
rm -rf buildWhat is | build? The | symbol introduces an order-only prerequisite. It means "make sure the build directory exists before running this rule, but don't rebuild just because the directory's timestamp changed." Without |, Make would rebuild everything whenever you add a new file to the build/ directory.
VPATH: Searching for Source Files
If your source files are in a different directory than where you run Make, VPATH tells Make where to look:
VPATH = src:lib
# Make will look for .c files in the current directory, then src/, then lib/
myapp: main.o utils.o
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@When Make needs main.c, it searches:
- Current directory — not found
src/— found! Usessrc/main.c- (Would check
lib/next if not found)
vpath (Lowercase) — More Precise
The lowercase vpath directive lets you specify search paths for specific file types:
vpath %.c src # Look for .c files in src/
vpath %.h include # Look for .h files in include/This is more precise than VPATH (uppercase), which applies to all files.
When to use VPATH vs separate directory pattern rules: For small projects, VPATH is simpler. For larger projects, explicit paths in pattern rules (like build/%.o: src/%.c) give you more control and are easier to understand.
Automatic Dependency Generation
Here's a common problem: your C files include header files, but Make doesn't know about these dependencies:
/* main.c */
#include "utils.h"
#include "config.h"If you change utils.h, Make won't know to recompile main.c because the Makefile only says main.o depends on main.c — it doesn't mention the headers.
The Solution: Let the Compiler Tell Make
The GCC (and Clang) compiler can generate dependency information automatically with the -MMD -MP flags:
CC := gcc
CFLAGS := -Wall -Wextra -MMD -MP
TARGET := myapp
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
build:
mkdir -p build
clean:
rm -rf build
# Include the dependency files (if they exist)
-include $(DEPS)How does this work? Let's trace through it step by step:
-
-MMDtells the compiler to create a.dfile alongside each.ofile. For example, compilingbuild/main.oalso createsbuild/main.d. -
The
.dfile contains dependency rules that look like this:build/main.o: src/main.c src/utils.h src/config.hThis tells Make that
main.odepends onmain.c,utils.h, andconfig.h. -
-MPadds empty "phony" rules for each header file. This prevents errors if you delete a header file — without-MP, Make would fail because it can't find the header that the old.dfile references. -
-include $(DEPS)reads all the.dfiles. The leading-means "don't error if the files don't exist" (they won't exist on the first build).
After the first build, the dependency files are generated. Now if you change utils.h, Make knows to recompile every .c file that includes it.
Putting It All Together
Here's a complete, production-ready Makefile that uses everything from this tutorial:
# --- Configuration ---
CC := gcc
CFLAGS := -Wall -Wextra -MMD -MP
LDFLAGS :=
LDLIBS :=
TARGET := myapp
# --- File Discovery ---
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)
# --- Build Rules ---
.PHONY: all clean info
all: $(TARGET)
$(TARGET): $(OBJS) | build
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
build:
mkdir -p build
clean:
rm -rf build
# Show what Make found
info:
@echo "Sources: $(SRCS)"
@echo "Objects: $(OBJS)"
@echo "Deps: $(DEPS)"
# Include auto-generated dependencies
-include $(DEPS)The info target is useful during development. Run make info to see which files Make discovered. This helps you debug issues where Make isn't finding your source files.
Summary
Here's what you've learned:
- Pattern rules (
%.o: %.c) replace repetitive per-file rules with a single generic rule $(wildcard *.c)finds files automatically- Substitution references (
$(SRCS:.c=.o)) andpatsubsttransform file name lists - Order-only prerequisites (
| dir) ensure directories exist without triggering rebuilds VPATH(uppercase) searches all file types;vpath(lowercase) searches specific patterns-MMD -MPflags auto-generate header dependency files (.dfiles)-include $(DEPS)reads those dependency files (silently ignoring missing ones)
In the next tutorial, you'll learn about Make's built-in functions — tools for manipulating strings, file names, and more.
Build a Project with Auto-Discovery and Dependencies
Create a Makefile for a project with this structure:
project/
src/
main.c
utils.c
parser.c
include/
utils.h
parser.h
build/ (generated)
Requirements:
- Auto-discover all
.cfiles insrc/ - Compile
.ofiles intobuild/ - Look for headers in
include/(use-I includeflag) - Auto-generate header dependencies
- Include an
infotarget that shows discovered files
Show Solution
CC := gcc
CFLAGS := -Wall -Wextra -I include -MMD -MP
TARGET := build/myapp
# Auto-discover source files
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)
.PHONY: all clean info
all: $(TARGET)
# Link
$(TARGET): $(OBJS) | build
$(CC) $^ -o $@
# Compile (pattern rule)
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
# Create build directory
build:
mkdir -p build
clean:
rm -rf build
info:
@echo "Sources: $(SRCS)"
@echo "Objects: $(OBJS)"
@echo "Dependencies: $(DEPS)"
# Include auto-generated dependency files
-include $(DEPS)Test it:
make info # See what files were discovered
make # Build the project
make clean # Remove build artifactsKey points:
-I includetells the compiler where to find header files$(wildcard src/*.c)automatically finds all source files$(patsubst src/%.c, build/%.o, $(SRCS))transformssrc/main.c→build/main.o| buildensures the directory exists without triggering unnecessary rebuilds-MMD -MPinCFLAGSauto-generates.ddependency files-include $(DEPS)reads those dependency files silently
Written by the ShellRAG Team
The ShellRAG editorial team writes practical, beginner-friendly Makefile tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.
Learn more about us →