Module 1: Functions and Metaprogramming
Functions in Make
Learning objectives
- Explain the core mental model behind Functions in Make
- Apply Functions in Make within Functions and Metaprogramming
- Identify important boundaries, trade-offs, and failure modes
- Produce concrete evidence from the practice exercise
Related: Variables | Pattern Rules | Conditionals | Make Index
Why Functions Exist in Make
Variables store values, but often you need to transform values — change extensions, filter a list, search for files, or compute a string. Make's built-in functions handle these transformations without shelling out:
SRCS := main.c utils.c parser.c
# Without functions — you'd have to list objects manually
OBJS := main.o utils.o parser.o
# With patsubst — derived automatically
OBJS := $(patsubst %.c,%.o,$(SRCS))
# Or the shorthand substitution reference:
OBJS := $(SRCS:.c=.o)Functions also let you query the filesystem, manipulate paths, and write conditional logic — turning Make into a real build language rather than just a rule executor.
Function Call Syntax
$(function-name arg1,arg2,arg3)
${function-name arg1,arg2,arg3} # alternative delimiters — less commonArguments are separated by commas. Spaces around commas are included in the argument (significant!):
$(subst a, b,text) # arg1 = "a", arg2 = " b", arg3 = "text" — space included!
$(subst a,b,text) # arg1 = "a", arg2 = "b", arg3 = "text" — correctString Substitution Functions
$(subst from,to,text)
Simple find-and-replace on literal strings:
result := $(subst .c,.o,main.c utils.c)
# result = main.o utils.o
result := $(subst cc,xx,$(CC))
# if CC = gcc → result = gxx$(patsubst pattern,replacement,text)
Pattern-based substitution using % as a wildcard for the stem:
SRCS := src/main.c src/utils.c
OBJS := $(patsubst src/%.c,build/%.o,$(SRCS))
# OBJS = build/main.o build/utils.o
# Shorthand substitution reference (equivalent to patsubst %.c,%.o):
OBJS := $(SRCS:.c=.o)$(strip text)
Removes leading and trailing whitespace, and collapses internal whitespace to single spaces:
X := " hello world "
Y := $(strip $(X))
# Y = "hello world"$(findstring find,text)
Returns find if it occurs in text, empty string otherwise:
has_debug := $(findstring debug,$(CFLAGS))
ifeq ($(has_debug),debug)
$(info Debug flags detected)
endifList and Word Functions
Make treats whitespace-separated words as lists. These functions operate on such lists.
$(filter pattern...,text)
Keep only words that match any of the patterns:
INPUT := foo.c bar.h baz.c qux.s
C_FILES := $(filter %.c,$(INPUT))
# C_FILES = foo.c baz.c
# Multiple patterns
C_AND_H := $(filter %.c %.h,$(INPUT))
# C_AND_H = foo.c bar.h baz.c$(filter-out pattern...,text)
Remove words that match — the complement of filter:
INPUT := main.o test.o debug.o release.o
NO_TEST := $(filter-out test.o debug.o,$(INPUT))
# NO_TEST = main.o release.o$(sort list)
Sorts words alphabetically and removes duplicates:
X := beta alpha gamma alpha
Y := $(sort $(X))
# Y = alpha beta gamma$(word n,text)
Returns the nth word (1-indexed):
X := one two three four
Y := $(word 2,$(X)) # Y = two$(words text)
Returns the number of words:
X := a b c d
N := $(words $(X)) # N = 4
LAST := $(word $(N),$(X)) # LAST = d$(firstword text) / $(lastword text)
X := alpha beta gamma
$(firstword $(X)) # alpha
$(lastword $(X)) # gamma$(wordlist start,end,text)
Returns words from start to end (1-indexed, inclusive):
X := a b c d e f
Y := $(wordlist 2,4,$(X)) # Y = b c dPath/Filename Functions
$(dir names)
Returns the directory part (with trailing /) of each path:
X := src/foo.c include/bar.h
Y := $(dir $(X)) # Y = src/ include/$(notdir names)
Returns the filename without the directory:
X := src/foo.c include/bar.h
Y := $(notdir $(X)) # Y = foo.c bar.h$(suffix names)
Returns the file extension (including .):
X := main.c utils.o build.mk
Y := $(suffix $(X)) # Y = .c .o .mk$(basename names)
Returns the filename without the extension:
X := src/main.c src/utils.o
Y := $(basename $(X)) # Y = src/main src/utils$(addsuffix suffix,names)
Appends a suffix to each word:
NAMES := main utils parser
SRCS := $(addsuffix .c,$(NAMES)) # SRCS = main.c utils.c parser.c$(addprefix prefix,names)
Prepends a prefix to each word:
NAMES := main.o utils.o
BUILT := $(addprefix build/,$(NAMES)) # BUILT = build/main.o build/utils.o$(join list1,list2)
Pairwise concatenation of two lists:
X := a b c
Y := 1 2 3
Z := $(join $(X),$(Y)) # Z = a1 b2 c3$(realpath names) / $(abspath names)
Convert paths to absolute paths. realpath resolves symlinks; abspath does not:
ABS := $(abspath ../include) # /home/user/project/includeFilesystem Function
$(wildcard pattern)
Expands a glob pattern against the actual filesystem (unlike % in rules, which Make expands):
SRCS := $(wildcard src/*.c)
# SRCS = src/main.c src/utils.c src/parser.c (whatever exists)
ALL_HEADERS := $(wildcard include/**/*.h) # recursive glob (GNU Make 4.3+)Critical difference: % in rules matches file names during rule lookup; $(wildcard ...) actually queries the filesystem at Makefile parse time.
Control Flow Functions
$(foreach var,list,text)
Iterates over a list, expanding text with var set to each word:
DIRS := src lib test
# Create mkdir commands for each
$(foreach d,$(DIRS),mkdir -p $(d);)
# Expands to: mkdir -p src; mkdir -p lib; mkdir -p test;
# More typical use — build a list
OBJS := $(foreach d,$(DIRS),$(wildcard $(d)/*.o))$(if condition,then[,else])
# If VERBOSE is non-empty, use verbose flag; else silent
VFLAG := $(if $(VERBOSE),-v,)
# Nested if
MODE := $(if $(DEBUG),debug,$(if $(RELEASE),release,dev))$(or cond1[,cond2,...])
Returns the first non-empty argument:
CC := $(or $(CC),$(shell which clang),gcc)
# Use CC if set, else find clang, else fall back to gcc$(and cond1[,cond2,...])
Returns the last argument if all are non-empty; otherwise empty:
# Only set EXTRA if both DEBUG and VERBOSE are set
EXTRA := $(and $(DEBUG),$(VERBOSE),-DDEBUG_VERBOSE)Shell Function
$(shell command)
Runs a shell command and returns its stdout (newlines replaced with spaces):
GIT_HASH := $(shell git rev-parse --short HEAD)
DATE := $(shell date +%Y%m%d)
NPROC := $(shell nproc)
$(info Building version $(GIT_HASH) on $(DATE))
# Use in a rule:
version.h:
echo "#define GIT_HASH \"$(GIT_HASH)\"" > $@Warning: $(shell ...) runs at parse time (when the variable is first evaluated), not when the rule runs. Side effects execute during Makefile loading.
$(call) — User-Defined Functions
Use define to create a named macro and $(call ...) to invoke it:
# Define a function: $(1), $(2), ... are positional arguments
define compile_c
$(CC) $(CFLAGS) -c -o $(1) $(2)
endef
# Call it
main.o: main.c
$(call compile_c,$@,$<)
# More practical: a function that builds a target+dep list
define make_obj
$(patsubst %.c,%.o,$(1))
endef
OBJS := $(call make_obj,$(SRCS))$(value) — Raw Variable Value
Returns a variable's value before recursive expansion — useful for debugging:
X = hello $(Y)
$(info value of X: $(value X)) # prints: hello $(Y) (not expanded)
$(info X: $(X)) # prints: hello world (fully expanded)$(eval) — Dynamic Rule Generation
$(eval ...) parses its argument as Makefile syntax at runtime. This enables dynamic rule generation:
define COMPILE_RULE
$(1).o: $(1).c
$(CC) -c -o $$@ $$<
endef
MODULES := main utils parser
$(foreach m,$(MODULES),$(eval $(call COMPILE_RULE,$(m))))
# Generates explicit rules for main.o, utils.o, parser.oNote the $$@ — $ must be doubled inside eval because it's expanded twice.
$(info), $(warning), $(error) — Diagnostic Output
$(info This is informational — printed, build continues)
$(warning Watch out — printed with filename:line, build continues)
$(error Fatal — printed with filename:line, build STOPS immediately)
# Useful for debugging variables:
$(info SRCS = $(SRCS))
$(info OBJS = $(OBJS))
# Guard against missing required variables:
ifndef API_KEY
$(error API_KEY is not set. Run: export API_KEY=...)
endifCommon Pitfalls
# PITFALL 1: Comma in argument — use a variable
COMMA := ,
result := $(subst $(COMMA), ,a,b,c) # replaces commas with spaces
# Can't use literal comma inside $(subst ) args without this trick
# PITFALL 2: $(wildcard) returns empty if nothing matches — no error
SRCS := $(wildcard *.nonexistent) # SRCS = "" — silent!
# Guard: ifeq ($(SRCS),) $(error No source files found) endif
# PITFALL 3: $(shell) side effects at parse time
RESULT := $(shell rm -f stale.o && echo removed) # runs immediately!
# PITFALL 4: Spaces in $(foreach) separator
X := $(foreach d,a b c,$(d)/) # X = a/ b/ c/ (spaces between)
# This is fine for lists, but be careful when using as shell argsPractice lab
Create a small repository that exercises Functions in Make. Capture a clean build, an incremental no-op build, and a deliberately broken dependency case; explain every command Make chooses to run. Add an operational constraint such as concurrency, recovery, security, latency, or cost, and defend the resulting design trade-off.
Review questions
- What problem does Functions in Make solve, and what assumptions does it rely on?
- Which boundary or failure case is easiest to miss, and how would you expose it?
- What alternative design would you consider, and what trade-off would change the decision?
- What artifact, trace, test, or metric proves that your implementation is correct?
Completion evidence
- A working artifact, annotated trace, or reproducible experiment
- At least one normal case and one deliberately failing or boundary case
- A concise explanation of the design choice and its trade-offs
- Saved output showing how correctness was evaluated