Search code examples
makefile

How do I write a Makefile target with a repeated pattern?


I have input files with a certain pattern that generates files with that pattern repeated.

my input files are:

inputs/<pattern>.py

e.g.

inputs/cfg1.py
inputs/cfg2.py

calling python inputs/<pattern>.py generates:

outputs/<pattern>/<pattern>.rpt
outputs/<pattern>/<pattern>.html
outputs/<pattern>/<pattern>.txt

e.g. python inputs/cfg1.py generates

outputs/cfg1/cfg1.rpt
outputs/cfg1/cfg1.html
outputs/cfg1/cfg1.txt

How do I write a make rule for that?

I tried

outputs/%/%.rpt: inputs/%.py
    python $<

but it doesn't work


Solution

  • With GNU make >= 4.3, using grouped targets (&:) and secondary expansion:

    I   := inputs
    O   := outputs
    in  := $(patsubst $I/%.py,%,$(wildcard $I/*.py))
    out := $(foreach i,$(in),$(foreach s,rpt html txt,$O/$i/$i.$s))
    
    .PHONY: all
    all: $(out)
    
    $O:
        mkdir -p $@
    
    .SECONDEXPANSION:
    
    $O/%.rpt $O/%.html $O/%.txt &: $I/$$(word 1,$$(subst /, ,$$*)).py | $O
        python $<
    

    Grouped targets (a b c &: p) is a way to indicate that the same run of the recipe creates several targets. It is needed to avoid running the same recipe 3 times for your rpt, html and txt targets.

    After the special .SECONDEXPANSION: all rules have their list of prerequisites expanded twice, and the second time the automatic variables, like $* for the stem of a pattern rule, are defined. So, this is a way to add some processing to the list of prerequisites. In your case to transform x/x into x.

    After the first expansion the rule above becomes:

    outputs/%.rpt outputs/%.html outputs/%.txt &: inputs/$(word 1,$(subst /, ,$*)).py | outputs
        python $<
    

    During the second expansion, for each inputs/x.py, it becomes (step by step):

    # step 1:
    outputs/x/x.rpt outputs/x/x.html outputs/x/x.txt &: inputs/$(word 1,$(subst /, ,x/x)).py | outputs
        python $<
    
    # step 2:
    outputs/x/x.rpt outputs/x/x.html outputs/x/x.txt &: inputs/$(word 1,x x).py | outputs
        python $<
    
    # step 3:
    outputs/x/x.rpt outputs/x/x.html outputs/x/x.txt &: inputs/x.py | outputs
        python $<
    

    For completeness, as suggested in another answer, you could obtain the same with eval, the make function that programmatically instantiates make statements. Here is a slightly different version of an eval-based solution:

    I   := inputs
    O   := outputs
    in  := $(patsubst $I/%.py,%,$(wildcard $I/*.py))
    
    .PHONY: all
    
    define my_rule
    all: $O/$1/$1.rpt $O/$1/$1.html $O/$1/$1.txt
    
    $O/$1/$1.rpt $O/$1/$1.html $O/$1/$1.txt &: $I/$1.py | $O/$1
        python $$<
    endef
    $(foreach i,$(in),$(eval $(call my_rule,$i)))
    
    $(addprefix $O/,$(in)):
        mkdir -p $@
    

    Note the $$< instead of $<, it is important, again because of double expansion.