Skip to content

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 make.

My Makefile declares the same tasks as the ones written in duties.py, 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 make check. Convenient, right?

Except that some duties (tasks) accept arguments. For example:

poetry run duty release version=0.1.2

So how do I allow the make equivalent?

make release version=0.1.2

Experiments¤

Do I write a specific rule in the Makefile for the release task?

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 make, 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!

Sorcery¤

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?!

Recipe¤

args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)"))

The heart of the magic. We declare a function called args. We later call it with $(call args,$@).

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

When running make docs-serve host=0.0.0.0, the 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 $(call args,$@), $@ is replaced by the rule name, which is docs-serve in this example, and host=0.0.0.0 is added to the command.

We successfully re-built the arguments passed on the command line!

Side-effects¤

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,$@)

Here rule1 and rule2 both accept a version argument.

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...

Addendum¤

Not a real addendum.

Share your tricks!