DEV Community

Timothée Mazzucotelli
Timothée Mazzucotelli

Posted on • Originally published at pawamoy.github.io

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 $@
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So how do I allow the make equivalent?

make release version=0.1.2
Enter fullscreen mode Exit fullscreen mode

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 $@
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
make release version=0.1.2
# => poetry run duty release version=0.1.2
Enter fullscreen mode Exit fullscreen mode

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,$@)
Enter fullscreen mode Exit fullscreen mode

What happens here?!

Recipe

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

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"
Enter fullscreen mode Exit fullscreen mode

This is why we declare our arguments like this:

check_args = files
docs_serve_args = host port
release_args = version
test_args = match
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,$@)
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

Addendum

Not a real addendum.

Share your tricks!

Top comments (0)