Robert D. French

blog(8) System Manager's Manual blog(8)

The make(1) command on OpenBSD allows you to define rules at runtime. This is to help make up for the fact that it does not support wildcard targets with the same flexibility as GNUUUUU Make. Here's what this looks like in practice:

# Find all section 1 manual entries in the current dir
man_pages != find . -type f -name "*.1"

# Turn each manual path into a path for an html page
web_pages = ${man_pages:S/$/.html/}

# By default, build all the web pages
build_all: $(web_pages)

# Since we do not have GNU-style wildcards, we generate
# a rule to build each web page
.for p in $(man_pages)
$p.html: $p
	mandoc -T html $p > $@
.endfor

For a simple example like this, we can accomplish the same thing with Inference Rules. Inference rules allow you to specify how files of one suffix can be transformed into files with a different suffix (for example, compiling ‘.o’ files from ‘.c’ files.) Here's what this same Makefile would look like with inference rules:

# Find all section 1 manual entries in the current dir
man_pages != find . -type f -name "*.1"

# Turn each manual path into a path for an html page
web_pages = ${man_pages:S/$/.html/}

# By default, build all the web pages
build_all: $(web_pages)

.SUFFIXES: .1 .html

# Transform ".1" (mdoc) files to html
.1.html:
	mandoc -T html $p > $@
The trouble arises when we want to consider manual entries of section 1. Let's adjust our find(1) command to show files ending in any digit (ignoring section ‘3p’ is fine, because Perl is gross):
# Find all UNIX manual entries, regardless of section
man_pages != find . -type f -name "*.[[:digit:]]"
...
If we want to handle this with inference rules, we would need separate recipes for manual section:
.SUFFIXES: .1 .2 .3 .4 .5 .6 .7 .8 .9 .html

# Transform ".1" (mdoc) files to html
.1.html:
	mandoc -T html $p > $@

# Transform ".2" (mdoc) files to html
.2.html:
	mandoc -T html $p > $@

# Transform ".3" (mdoc) files to html
.3.html:
	mandoc -T html $p > $@

...
However, with the dynamic recipe approach in the first example, we don't need separate recipes for each section. The “for loop” will create a separate rule for each page discovered by find(1). Here is the full example:
# Find all UNIX manual entries, regardless of section
man_pages != find . -type f -name "*.[[:digit:]]"

# Turn each manual path into a path for an html page
web_pages = ${man_pages:S/$/.html/}

# Build all the web pages
build_all: $(web_pages)

# Generate a recipe for each man page, regardless of section
.for p in $(man_pages)
$p.html: $p
	mandoc -T html $p > $@
.endfor
2025-05-21 Robert D. French