Passing Makefile arguments to a command, as they are typed in the command line.
In my Python projects,
I use a combination of Duty
(my task runner) and
Makefile declares the same tasks as the ones written in
but in a generic manner:
TASKS = check test release .PHONY: $(TASKS) $(TASKS): @poetry run duty $@
So, instead of running
poetry run duty check, I can run
Except that some duties (tasks) accept arguments. For example:
poetry run duty release version=0.1.2
So how do I allow the
make release version=0.1.2
Do I write a specific rule in the Makefile for the
TASKS = check test release .PHONY: release release: @poetry run duty release version=$(version) .PHONY: $(TASKS) $(TASKS): @poetry run duty $@
Meh. My Makefile rules are not generic anymore.
Besides, what if the argument is optional?
If I don't pass it when running
the command will end up being
poetry run duty release version=,
which is just wrong.
Instead, I'd like to find a generic way to insert the arguments in the command just as they are typed on the command line:
make release # => poetry run duty release
make release version=0.1.2 # => poetry run duty release version=0.1.2
Well, after a few hours playing with Makefiles features, I got a nice solution!
Let me sprinkle this dark magic right here:
args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_args = files docs_serve_args = host port release_args = version test_args = match TASKS = \ check \ docs-serve \ release \ test .PHONY: $(TASKS) $(TASKS): @poetry run duty $@ $(call args,$@)
What happens here?!
args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)"))
The heart of the magic. We declare a function called
We later call it with
It could be described like this:
args reference := first parameter replace - by _ in args reference append "_args" to args reference get argument names by dereferencing args reference for each argument name get argument value by dereferencing argument name if argument value is not empty print "argument name = argument value"
This is why we declare our arguments like this:
check_args = files docs_serve_args = host port release_args = version test_args = match
make docs-serve host=0.0.0.0,
args function will do the following:
args_ref := "docs-serve" args_ref becomes "docs_serve" (replace) args_ref becomes "docs_serve_args" args_names is value of "docs_serve_args" variable args_names therefore is "host port" arg "host": variable "host" is not empty print "host=0.0.0.0" arg "port": variable "port" is empty print nothing
So when calling
$@ is replaced
by the rule name, which is
docs-serve in this example,
host=0.0.0.0 is added to the command.
We successfully re-built the arguments passed on the command line!
Arguments can be passed to
make in no particular order.
The following commands are all equivalent:
make hello world=earth foo bar=baz make hello foo bar=baz world=earth make bar=baz hello foo world=earth
It can be seen as an advantage but as an inconvenient as well, because you cannot have arguments with the same name for different commands. Or at least, you could not use these commands and arguments at the same time.
args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) rule1_args = version rule2_args = version name TASKS = rule1 rule2 .PHONY: $(TASKS) $(TASKS): @poetry run duty $@ $(call args,$@)
rule2 both accept a
make rule1 version=1 # OK make rule1 version=1 rule2 # not OK # it will result in "poetry run duty rule1 version=1" # then "poetry run duty rule2 version=1"! make rule1 version=1 rule2 version=2 # ??? # I couldn't get the courage to try more of these dark arts # so I don't know what would happen here...
Not a real addendum.
Share your tricks!