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.
1
Build Systems Foundations
Why Make, compilation pipeline, basics
2
Targets, Prerequisites & Execution
Rule anatomy, PHONY, build workflows
3
Variables, Expansion & Scope
= vs :=, ?=, +=, CLI overrides
4
Automatic Variables & Pattern Rules
$@, $<, $^, $?, %.o: %.c patterns
5
Built-in Functions & Make Language
subst, wildcard, foreach, $(shell)
You Are Here
6
Conditionals & Configurable Builds
ifeq, ifdef, platform detection, flags
7
Automatic Dependency Generation
-M, -MM, -MD, .d files, header deps
8
Compilation Workflow & Libraries
Static/shared libs, ar, -fPIC, SONAME
9
Project Architecture & Multi-Directory
Recursive make, include, out-of-source
10
Cross-Compilation & Toolchains
Toolchain prefixes, sysroots, embedded
11
Parallel Builds & Performance
make -j, jobserver, race conditions
12
Testing, Coverage & Debug Tooling
Test targets, gcov/lcov, sanitizers
13
Make as Automation & DevOps Tool
Task runner, Docker, install, packaging
14
CI/CD Integration
Deterministic builds, GitHub Actions, cache
15
Advanced Make & Debugging
--debug, dynamic rules, evaluation traps
16
Ecosystem, Alternatives & Mastery
CMake, Ninja, Meson, Bazel, production
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.
Continue the Series
Part 6: Conditionals & Configurable Builds
Use ifeq, ifdef, and OS detection to write portable, multi-configuration Makefiles for debug, release, and cross-platform builds.
Read Article
Part 7: Automatic Dependency Generation
Solve the header-change problem: use -M/-MD to auto-generate .d dependency files and include them so Make rebuilds exactly what changed.
Read Article
Part 4: Automatic Variables & Pattern Rules
Review $@, $<, $^, $*, and pattern rules that compile all sources automatically without repetition.
Read Article