Back to Technology

GNU Make Mastery Part 5: Built-in Functions & Make Language

February 21, 2026 Wasil Zafar 24 min read

Unlock Make's full expressive power with its built-in function library: string manipulation, file globbing, list processing, user-defined call functions, eval for dynamic rule generation, and $(shell) for system integration.

Table of Contents

  1. String Functions
  2. File & Path Functions
  3. Iteration: foreach
  4. User Functions: call
  5. eval & value
  6. $(shell) — System Integration
  7. Hands-On Milestone
  8. Next Steps

String Functions

Part 5 of 16 — GNU Make Mastery Series. With variables (Part 3) and pattern rules (Part 4) under your belt, we now explore the Make standard library — functions that transform strings, discover files, and generate rules dynamically.

Make functions are called with $(function arg1,arg2,...). Note: commas and spaces inside arguments must be escaped — assign them to variables first if needed: COMMA:=, and SPACE:= $(EMPTY).

subst — Simple Text Substitution

# $(subst from,to,text)
SRCS := src/main.c src/utils.c src/network.c
OBJS := $(subst src/,build/,$(SRCS:.c=.o))
# → build/main.o build/utils.o build/network.o

patsubst — Pattern Substitution

# $(patsubst pattern,replacement,text) — % is the wildcard
SRCS := main.c utils.c  network.c
OBJS := $(patsubst %.c,%.o,$(SRCS))       # → main.o utils.o network.o
DEPS := $(patsubst %.c,build/%.d,$(SRCS)) # → build/main.d build/utils.d build/network.d

# Shorthand — variable substitution reference:
OBJS := $(SRCS:.c=.o)    # equivalent to patsubst %.c,%.o,...

filter and filter-out

FILES  := main.c utils.c README.md Makefile notes.txt server.c
C_FILES := $(filter %.c,$(FILES))          # → main.c utils.c server.c
NON_C  := $(filter-out %.c,$(FILES))       # → README.md Makefile notes.txt

# Use case: split test files from production files
ALL_OBJS  := main.o utils.o test_main.o test_utils.o
PROD_OBJS := $(filter-out test_%.o,$(ALL_OBJS))    # → main.o utils.o
TEST_OBJS := $(filter     test_%.o,$(ALL_OBJS))    # → test_main.o test_utils.o

sort, strip, words, word

LIST  := banana apple cherry apple date banana

SORTED := $(sort $(LIST))               # → apple banana cherry date  (deduplicates!)
WORDS  := $(words $(LIST))              # → 6
FIRST  := $(word 1,$(LIST))             # → banana
SUBLIST := $(wordlist 2,4,$(LIST))      # → apple cherry apple

# strip — remove leading/trailing whitespace
VAR := $(strip   lots of spaces   )    # → lots of spaces

Sample Source Files

Self-contained examples: Built-in function examples reference main.c, utils.c, net.c, and server.c. These trivial stubs let you paste any Makefile snippet, run make, and see the function expand correctly.

main.c

/* main.c — entry point */
#include <stdio.h>
#include "utils.h"

int main(void) {
    utils_greet("Built-in Functions");
    return 0;
}

utils.c

/* utils.c */
#include <stdio.h>

void utils_greet(const char *name) {
    printf("Hello, %s!\n", name);
}

net.c

/* net.c — minimal networking stub */
#include <stdio.h>

void net_init(void)  { printf("net: initialised\n"); }
void net_close(void) { printf("net: closed\n"); }

server.c

/* server.c — server main loop stub */
#include <stdio.h>

void server_start(int port) {
    printf("Server starting on port %d ...\n", port);
}

void server_stop(void) {
    printf("Server stopped.\n");
}

network.c

/* network.c — higher-level network wrapper */
#include <stdio.h>

void network_connect(const char *addr) {
    printf("Connecting to %s\n", addr);
}

File & Path Functions

wildcard

Performs shell-style glob expansion at parse time. Unlike a shell glob in a recipe, this works in variable assignments and prerequisites.

# Discover all source files automatically — no manual list needed
SRCS := $(wildcard src/*.c)                    # → src/main.c src/utils.c ...
SRCS += $(wildcard src/**/*.c)                 # requires GNU Make 3.81+

# Guard against empty wildcard (avoid "no input files" compiler error)
ifeq ($(SRCS),)
    $(error No .c files found in src/)
endif

dir, notdir, basename, suffix

PATH1 := src/net/tcp.c

DIR    := $(dir     $(PATH1))   # → src/net/
FILE   := $(notdir  $(PATH1))   # → tcp.c
BASE   := $(basename $(PATH1))  # → src/net/tcp
EXT    := $(suffix  $(PATH1))   # → .c

# Real usage: map src/*.c → build/obj/*.o preserving subdirectory structure
SRCS := $(wildcard src/**/*.c src/*.c)
OBJS := $(patsubst src/%.c,build/obj/%.o,$(SRCS))

addprefix and addsuffix

MODULES := core net io ui
SRCS    := $(addprefix src/,$(addsuffix .c,$(MODULES)))
# → src/core.c src/net.c src/io.c src/ui.c

INCDIRS := include vendor/include third_party/include
CFLAGS  += $(addprefix -I,$(INCDIRS))
# → -Iinclude -Ivendor/include -Ithird_party/include

Iteration: foreach

$(foreach var,list,text) iterates over a whitespace-separated list, expanding text with $(var) set to each word. The results are space-joined.

PLATFORMS := linux macos windows
TARGETS   := $(foreach p,$(PLATFORMS),build/$(p)/myapp)
# → build/linux/myapp build/macos/myapp build/windows/myapp

# Generate -I flags for all subdirectories of include/
INCDIRS  := $(wildcard include/*)
CFLAGS   += $(foreach d,$(INCDIRS),-I$(d))   # -Iinclude/core -Iinclude/net ...

# Create build directories for all modules
MODULES  := core net io ui
BUILDDIRS := $(foreach m,$(MODULES),$(BUILDDIR)/$(m))

.PHONY: makedirs
makedirs:
	$(foreach d,$(BUILDDIRS),mkdir -p $(d);)  # semicolons chain commands in one shell

User Functions: call

Define a reusable macro with define using positional parameters $(1), $(2)…, then invoke it with $(call name,arg1,arg2,...).

# A function that produces -I flags from a directory list
include_flags = $(foreach d,$(1),-I$(d))

# A function that maps sources to object paths
src_to_obj = $(patsubst $(1)/%.c,$(2)/%.o,$(3))

# Usage
INCDIRS := include vendor/include
CFLAGS  += $(call include_flags,$(INCDIRS))

OBJS := $(call src_to_obj,src,build,$(SRCS))
# Recursive function: generate per-module build rules dynamically
define MODULE_RULES
$(2)/$(1).o: src/$(1).c
	$(CC) $(CFLAGS) -Isrc/$(1) -c $$< -o $$@
endef
# NOTE: $$< and $$@ use double-dollar to survive two expansion rounds:
#   1st by $(call)  →  $<  $@
#   2nd by $(eval)  →  actual automatic variable values at recipe time

MODULES := core net ui
$(foreach m,$(MODULES),$(eval $(call MODULE_RULES,$(m),$(BUILDDIR))))

eval & value

$(eval text) parses text as Makefile syntax — letting you generate rules and variable assignments programmatically. $(value var) returns the raw (unexpanded) value of a variable.

# Generate a build rule for each listed binary
BINARIES := server client admin-tool

define BINARY_RULE
$(1): $(1).o common.o
	$(CC) $(LDFLAGS) -o $$@ $$^
endef

$(foreach bin,$(BINARIES),$(eval $(call BINARY_RULE,$(bin))))
Escape rule: Inside a define block used with $(eval $(call ...)), dollar signs in recipe lines must be doubled ($$@, $$<) because the text goes through two rounds of expansion: first by $(call), then by $(eval).

$(shell) — System Integration

$(shell command) executes a shell command at Makefile parse time and substitutes its output (with newlines converted to spaces). Use it sparingly — it runs on every make invocation.

# Get the current git commit hash for embedding in the binary
GIT_HASH  := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
VERSION   := $(shell cat VERSION 2>/dev/null || echo 0.0.0)
BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

CFLAGS += -DGIT_HASH='"$(GIT_HASH)"'
CFLAGS += -DVERSION='"$(VERSION)"'

# Detect the number of CPU cores for -j
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)

# Discover all headers in the system
SYS_INCLUDES := $(shell pkg-config --cflags openssl 2>/dev/null)
SYS_LIBS     := $(shell pkg-config --libs   openssl 2>/dev/null)

Hands-On Milestone

Milestone: Write a self-configuring Makefile that auto-discovers all source files, constructs include flags dynamically, embeds git metadata, and generates per-module build rules.
# Self-configuring Makefile using Make functions

# ── Variables ────────────────────────────────────────
CC       := gcc
CFLAGS   := -Wall -Wextra -std=c11 -O2
SRCDIR   := src
BUILDDIR := build
TARGET   := myapp

# ── Auto-discover sources ───────────────────────────
SRCS  := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/**/*.c)   # find all .c files
OBJS  := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))       # map src/ → build/

# ── Auto-generate include flags ─────────────────────
INCDIRS := $(wildcard include/*) include
CFLAGS  += $(addprefix -I,$(INCDIRS))                            # -Iinclude/core -Iinclude ...

# ── Embed build metadata ────────────────────────────
GIT_HASH  := $(shell git rev-parse --short HEAD 2>/dev/null || echo nogit)
CFLAGS    += -DGIT_HASH='"$(GIT_HASH)"'

.PHONY: all clean info

all: $(TARGET)

# ── Link ──
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^                # $^ = all .o prerequisites

# ── Compile ──
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
	@mkdir -p $(dir $@)                       # $(dir $@) = directory part of the .o path
	$(CC) $(CFLAGS) -c $< -o $@              # $< = source file, $@ = object file

clean:
	rm -rf $(BUILDDIR) $(TARGET)

info:
	@echo "CC       = $(CC)"
	@echo "CFLAGS   = $(CFLAGS)"
	@echo "TARGET   = $(TARGET)"
	@echo "SRCDIR   = $(SRCDIR)"
	@echo "BUILDDIR = $(BUILDDIR)"
	@echo "SRCS     = $(SRCS)"
	@echo "OBJS     = $(OBJS)"
	@echo "INCDIRS  = $(INCDIRS)"
	@echo "GIT_HASH = $(GIT_HASH)"
	@echo "Source count: $(words $(SRCS)) files"
# --- Setup: create source files for the self-configuring Makefile ---
mkdir -p src include/utils include/net

cat > src/main.c << 'EOF'
#include <stdio.h>
#include "utils.h"

int main(void) {
    utils_greet("Make Functions");
    return 0;
}
EOF

cat > src/utils.c << 'EOF'
#include <stdio.h>
#include "utils.h"
void utils_greet(const char *name) { printf("Hello, %s!\n", name); }
EOF

cat > include/utils/utils.h << 'EOF'
#ifndef UTILS_H
#define UTILS_H
void utils_greet(const char *name);
#endif
EOF

cat > include/net/net.h << 'EOF'
#ifndef NET_H
#define NET_H
void net_connect(const char *host, int port);
#endif
EOF
# --- Try it ---
make info     # display auto-discovered sources, include dirs, git hash
make          # compiles src/*.c → build/*.o → links myapp
make          # nothing rebuilt (up-to-date)
make clean    # remove build artifacts

Next in the Series

In Part 6: Conditionals & Configurable Builds, we make Makefiles smart — using ifeq, ifdef, OS detection, and feature flags to build the same project for debug, release, Linux, macOS, and embedded targets from one Makefile.

Technology