Functions
Summary: in this tutorial, you will learn learn make's built-in functions for manipulating strings, file names, and lists — from everyday helpers to advanced metaprogramming with eval and call.
Functions
Make has built-in functions that transform text. Since everything in Make is a string (file names, compiler flags, lists of files), these functions are how you manipulate and process data inside your Makefile.
What you'll learn in this tutorial:
- How to call Make functions using
$(function arguments) - The string functions you'll use most often (
subst,patsubst,strip,findstring) - File name functions (
dir,notdir,basename,suffix,addprefix,addsuffix) - List filtering functions (
filter,filter-out,sort,word,words) - The
$(shell ...)function for running terminal commands - Looping with
$(foreach ...) - Advanced functions:
$(call ...),$(eval ...), and user-defined functions
Which functions will you actually use 90% of the time? In practice, most Makefiles only need: wildcard, patsubst, addprefix, addsuffix, filter, filter-out, dir, notdir, shell, and foreach. The rest are situational. We'll mark the everyday functions so you know which to focus on.
How Function Calls Work
All Make functions follow the same syntax:
$(function-name argument1,argument2,argument3)Important rules:
- No space between the function name and the opening parenthesis
- Arguments are separated by commas
- Spaces matter —
$(subst a, b, text)has a space beforeband beforetext, which becomes part of the replacement! Usually you don't want this.
# WRONG — extra spaces become part of the values
RESULT := $(subst .c, .o, main.c) # Result: " .o" (note the space!)
# RIGHT — no extra spaces
RESULT := $(subst .c,.o,main.c) # Result: ".o"Common mistake: extra spaces in function arguments. Unlike most programming languages, spaces around commas in Make functions are significant. Write $(patsubst %.c,%.o,$(SRCS)) — not $(patsubst %.c, %.o, $(SRCS)).
String Functions
subst — Find and Replace ⭐
Replaces every occurrence of one string with another:
$(subst from,to,text)FILES := main.c utils.c config.c
RESULT := $(subst .c,.o,$(FILES))
# RESULT: main.o utils.o config.opatsubst — Pattern Substitution ⭐
Like subst, but uses % as a wildcard:
$(patsubst pattern,replacement,text)SRCS := src/main.c src/utils.c
OBJS := $(patsubst src/%.c,build/%.o,$(SRCS))
# OBJS: build/main.o build/utils.opatsubst vs subst: Use subst for simple text replacement (no wildcards). Use patsubst when you need to transform file paths with % as a wildcard. The shorthand $(VAR:.c=.o) is equivalent to $(patsubst %.c,%.o,$(VAR)).
strip — Remove Extra Whitespace
MESSY := " hello world "
CLEAN := $(strip $(MESSY))
# CLEAN: "hello world"findstring — Check if a String Exists
Returns the string if found, or empty if not:
$(findstring needle,haystack)# Check if "debug" appears in the CFLAGS
ifneq ($(findstring -g,$(CFLAGS)),)
$(info Debug mode is enabled)
endiffilter — Keep Matching Words ⭐
Keeps only words that match a pattern:
$(filter pattern,text)FILES := main.c utils.h config.c parser.h
C_FILES := $(filter %.c,$(FILES))
# C_FILES: main.c config.c
H_FILES := $(filter %.h,$(FILES))
# H_FILES: utils.h parser.hfilter-out — Remove Matching Words ⭐
The opposite of filter — removes words that match:
$(filter-out pattern,text)SRCS := main.c test_main.c utils.c test_utils.c
PROD_SRCS := $(filter-out test_%,$(SRCS))
# PROD_SRCS: main.c utils.csort — Sort and Remove Duplicates
DIRS := src lib src include lib
UNIQUE := $(sort $(DIRS))
# UNIQUE: include lib src (sorted alphabetically, duplicates removed)word and words — Access Individual Words
LIST := apple banana cherry
FIRST := $(word 1,$(LIST)) # apple
SECOND := $(word 2,$(LIST)) # banana
COUNT := $(words $(LIST)) # 3
LAST := $(word $(words $(LIST)),$(LIST)) # cherryFile Name Functions
These functions work with file paths — extracting parts, changing extensions, etc.
dir and notdir — Split Path and File Name ⭐
FILES := src/main.c lib/utils.c
DIRS := $(dir $(FILES))
# DIRS: src/ lib/
NAMES := $(notdir $(FILES))
# NAMES: main.c utils.cbasename and suffix — Split Name and Extension
FILES := src/main.c lib/utils.h
BASES := $(basename $(FILES))
# BASES: src/main lib/utils
EXTS := $(suffix $(FILES))
# EXTS: .c .haddprefix and addsuffix ⭐
Add text to the beginning or end of each word:
NAMES := main utils config
WITH_PREFIX := $(addprefix src/,$(NAMES))
# WITH_PREFIX: src/main src/utils src/config
WITH_SUFFIX := $(addsuffix .c,$(NAMES))
# WITH_SUFFIX: main.c utils.c config.c
# Combine both:
FULL_PATHS := $(addprefix src/,$(addsuffix .c,$(NAMES)))
# FULL_PATHS: src/main.c src/utils.c src/config.cwildcard — Find Files ⭐
You've already seen this one. It expands file name patterns:
SRCS := $(wildcard src/*.c)
HDRS := $(wildcard include/*.h)
ALL := $(wildcard src/*.c src/*.h)realpath and abspath — Full File Paths
# realpath: resolves symlinks, file must exist
FULL := $(realpath src/main.c)
# FULL: /home/user/project/src/main.c
# abspath: doesn't resolve symlinks, file doesn't need to exist
FULL := $(abspath build/output)
# FULL: /home/user/project/build/outputThe shell Function ⭐
Runs a shell command and captures its output:
$(shell command)# Get today's date
DATE := $(shell date +%Y-%m-%d)
# Count source files
NUM_SRCS := $(shell ls src/*.c 2>/dev/null | wc -l)
# Get the git commit hash
GIT_HASH := $(shell git rev-parse --short HEAD)
# Find all .c files recursively (works better than wildcard for deep trees)
SRCS := $(shell find src -name '*.c')$(shell ...) runs at Makefile parse time, not when a rule's recipe runs. This means the command executes when Make reads the Makefile, even if you never build anything. Don't put slow commands here unless necessary.
Dollar signs in shell commands: If your shell command uses $, you need to double it as $$ in a Makefile recipe. But inside $(shell ...), you only need single $ for shell variables. This is one of Make's more confusing aspects:
# In a variable definition with $(shell)
COUNT := $(shell echo $HOME) # Works — shell sees $HOME
# In a recipe (rule body)
info:
echo $$HOME # Need $$ — Make eats the first $foreach — Looping ⭐
Applies a template to each item in a list:
$(foreach variable,list,template)DIRS := src lib tests
CLEAN_RULES := $(foreach d,$(DIRS),rm -rf $(d)/*.o;)
# CLEAN_RULES: rm -rf src/*.o; rm -rf lib/*.o; rm -rf tests/*.o;A more practical example — creating -I flags for every include directory:
INCLUDE_DIRS := include src/headers lib/api
INCLUDE_FLAGS := $(foreach d,$(INCLUDE_DIRS),-I$(d))
# INCLUDE_FLAGS: -Iinclude -Isrc/headers -Ilib/apiHow foreach works: For each word in the list, Make sets variable to that word and expands the template. The results are joined with spaces. In the example above, d takes the values include, src/headers, and lib/api in turn.
User-Defined Functions with call
You can create your own reusable functions using define and call them with $(call ...):
# Define a function (using $1, $2 for parameters)
define compile_message
@echo "Compiling $1 → $2"
endef
# Define a more useful function
define make_object_rule
$(2)/$(notdir $(1:.c=.o)): $(1)
$$(CC) $$(CFLAGS) -c $$< -o $$@
endefCall the function:
# $1 gets "main.c", result: @echo "Compiling main.c → main.o"
$(call compile_message,main.c,main.o)Double dollar signs in define blocks used with call: When you define a function that generates Makefile rules, you need $$ for things that should be expanded later (like $$@, $$<). Using single $ would cause them to be expanded when call processes the function, before the rule is actually created. This is confusing at first — don't worry, you rarely need call in practice.
When to Use call
call is useful when you have repetitive patterns that differ by a parameter:
# Function: create a build rule for a library
define make_lib_rule
build/lib$(1).a: $(wildcard $(1)/src/*.c)
$$(CC) $$(CFLAGS) -c $$^ -o $$@
endef
# Generate rules for multiple libraries
$(eval $(call make_lib_rule,json))
$(eval $(call make_lib_rule,http))
$(eval $(call make_lib_rule,utils))eval — Advanced Metaprogramming
$(eval ...) takes text and processes it as Makefile syntax. It's the most powerful — and most confusing — function in Make.
$(eval text)The text you pass to eval becomes part of the Makefile, as if you had written it there manually.
# This:
$(eval CC := clang)
# Is the same as writing:
CC := clangCombining eval, call, and foreach
The real power comes from generating rules dynamically:
MODULES := auth database api
# Function: generate rules for one module
define MODULE_RULES
$(1)_SRCS := $$(wildcard modules/$(1)/*.c)
$(1)_OBJS := $$(patsubst %.c,build/%.o,$$($(1)_SRCS))
build/lib$(1).a: $$($(1)_OBJS)
ar rcs $$@ $$^
endef
# Generate rules for ALL modules
$(foreach m,$(MODULES),$(eval $(call MODULE_RULES,$(m))))You probably don't need eval. Most projects work fine without it. eval is for large, complex build systems where you need to generate dozens of similar rules from data. If you're just starting out, skip this section and come back when (if) you need it.
info, warning, and error — Debug Messages
These functions print messages during Makefile parsing:
$(info Building version $(VERSION)) # Just prints the message
$(warning CFLAGS might be too strict) # Prints with file:line and "warning:" prefix
$(error Missing required variable FOO) # Prints error and STOPS Make immediately# Practical use: validate that required variables are set
ifndef CC
$(error CC is not set. Please define your C compiler)
endif
# Informational output
$(info Using compiler: $(CC))
$(info Source files: $(SRCS))Quick Reference Table
| Function | What It Does | Example |
|---|---|---|
$(subst a,b,text) | Replace a with b | $(subst .c,.o,main.c) → main.o |
$(patsubst %a,%b,text) | Pattern replace | $(patsubst %.c,%.o,main.c) → main.o |
$(strip text) | Remove extra spaces | $(strip a b ) → a b |
$(filter %.c,files) | Keep matching | $(filter %.c,a.c b.h) → a.c |
$(filter-out %.h,files) | Remove matching | $(filter-out %.h,a.c b.h) → a.c |
$(sort list) | Sort + deduplicate | $(sort b a b) → a b |
$(dir path) | Directory part | $(dir src/a.c) → src/ |
$(notdir path) | File name part | $(notdir src/a.c) → a.c |
$(basename path) | Remove extension | $(basename a.c) → a |
$(suffix path) | Get extension | $(suffix a.c) → .c |
$(addprefix p,list) | Add prefix | $(addprefix src/,a b) → src/a src/b |
$(addsuffix s,list) | Add suffix | $(addsuffix .c,a b) → a.c b.c |
$(wildcard pat) | Find files | $(wildcard *.c) → matching files |
$(shell cmd) | Run shell command | $(shell date) → today's date |
$(foreach v,list,body) | Loop over list | See examples above |
$(call func,a,b) | Call user function | See examples above |
$(eval text) | Parse as Makefile | See examples above |
Summary
Here's what you've learned:
- Make functions use
$(name args)syntax — spaces in arguments matter - Everyday functions:
wildcard,patsubst,addprefix,addsuffix,filter,filter-out,shell,foreach - File name functions split paths:
dir,notdir,basename,suffix $(shell ...)runs commands at parse time and captures output$(foreach ...)loops over a list, applying a template to each item$(call ...)and$(eval ...)are advanced — they let you create reusable functions and generate rules dynamically$(info ...),$(warning ...),$(error ...)help with debugging
In the next tutorial, you'll learn about conditionals — making your Makefile behave differently based on the platform, build mode, or other conditions.
Build a Multi-Library Project with Functions
Write a Makefile that:
- Auto-discovers all
.cfiles insrc/ - Separates them into "library" files (starting with
lib_) and "main" files (everything else) - Creates include flags for both
include/andvendor/include/directories usingforeach - Has an
infotarget that shows all discovered files and computed values
Assume this project structure:
src/
main.c
app.c
lib_json.c
lib_http.c
include/
vendor/include/
Show Solution
CC := gcc
CFLAGS := -Wall -Wextra
# Auto-discover all source files
ALL_SRCS := $(wildcard src/*.c)
# Separate library files from application files
LIB_SRCS := $(filter src/lib_%,$(ALL_SRCS))
APP_SRCS := $(filter-out src/lib_%,$(ALL_SRCS))
# Generate object file lists
LIB_OBJS := $(patsubst src/%.c,build/%.o,$(LIB_SRCS))
APP_OBJS := $(patsubst src/%.c,build/%.o,$(APP_SRCS))
ALL_OBJS := $(LIB_OBJS) $(APP_OBJS)
# Generate include flags
INC_DIRS := include vendor/include
INC_FLAGS := $(foreach d,$(INC_DIRS),-I$(d))
CFLAGS += $(INC_FLAGS)
# Output
TARGET := build/myapp
.PHONY: all clean info
all: $(TARGET)
$(TARGET): $(ALL_OBJS) | build
$(CC) $^ -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
build:
mkdir -p build
clean:
rm -rf build
info:
@echo "=== Project Info ==="
@echo "All sources: $(ALL_SRCS)"
@echo "Lib sources: $(LIB_SRCS)"
@echo "App sources: $(APP_SRCS)"
@echo "Lib objects: $(LIB_OBJS)"
@echo "App objects: $(APP_OBJS)"
@echo "Include dirs: $(INC_DIRS)"
@echo "Include flags:$(INC_FLAGS)"
@echo "CFLAGS: $(CFLAGS)"Key points:
$(filter src/lib_%,...)selects only files matching thelib_prefix$(filter-out src/lib_%,...)selects everything EXCEPTlib_files$(foreach d,$(INC_DIRS),-I$(d))generates-Iinclude -Ivendor/include- The
infotarget helps you verify that all the function calls produce the expected results
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 →