story
%: %.ct
cpp -P $(shell env | sed -e "s/.*/-D_ENV_'&'/") $< > $@
%: %.st
( export __FILE__=$< ; echo "cat <<!" ; cat $< ; echo "!" ) | bash > $@
%: %.mt
rm -f $@.err
m4 -D__FILE__=$< $(shell env | sed -e "s/\([^=]*\)=\(.*\)/-D'_ENV_\\1=\\2'/") $< > $@
if [ -s $@.err ] ; then cat $@.err ; exit 1 ; fi
rm -f $@.err
Explanation: this gives you three "flavors" of preprocessing. ".ct" files are "C preprocessor templates"; they are put through cpp. ".mt" files are M4 templates: M4 is used. And ".st" are shell templates; they use shell here-doc syntax.The shell templates do not have to have any "cat <<" or "!"; this wrapping is added by the Makefile. Of course, you can't have a line consisting of ! in these files; you have to escape such a thing if it occurs.
Additionally, for .ct and .mt files, all environment variables are turned into macros in that language, with their names mapped to the the _ENV_* namespace. For .st and .mt files, the __FILE__ macro is established which expands to the file name (the C preprocessor has this built-in).
I used this in an embedded Linux distro that I build from scratch (targetting MIPS embedded hardware). It was used for generating some of the textual materials in the target file system tree. For instance /etc/hosts was generated from a .st template, and populated using a $(for ...) loop.
Yes, I do Lisp and I do stupid. :)