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 VPATH and vpath help Make find files in other directories
  • How to generate header dependencies automatically (so changing a .h file 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.o

All 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:

  1. Make needs main.o
  2. It matches the pattern %.o, so % = main
  3. The prerequisite becomes main.c (replacing % with main)
  4. $< = main.c, $@ = main.o
  5. 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.o

The 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.o

patsubst 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 build

What 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:

  1. Current directory — not found
  2. src/ — found! Uses src/main.c
  3. (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:

  1. -MMD tells the compiler to create a .d file alongside each .o file. For example, compiling build/main.o also creates build/main.d.

  2. The .d file contains dependency rules that look like this:

    build/main.o: src/main.c src/utils.h src/config.h

    This tells Make that main.o depends on main.c, utils.h, and config.h.

  3. -MP adds 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 .d file references.

  4. -include $(DEPS) reads all the .d files. 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)) and patsubst transform 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 -MP flags auto-generate header dependency files (.d files)
  • -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:

  1. Auto-discover all .c files in src/
  2. Compile .o files into build/
  3. Look for headers in include/ (use -I include flag)
  4. Auto-generate header dependencies
  5. Include an info target 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 artifacts

Key points:

  • -I include tells the compiler where to find header files
  • $(wildcard src/*.c) automatically finds all source files
  • $(patsubst src/%.c, build/%.o, $(SRCS)) transforms src/main.cbuild/main.o
  • | build ensures the directory exists without triggering unnecessary rebuilds
  • -MMD -MP in CFLAGS auto-generates .d dependency files
  • -include $(DEPS) reads those dependency files silently
Was this page helpful?
SR

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 →