[makefile] Passing arguments to "make run"

I use Makefiles.

I have a target called run which runs the build target. Simplified, it looks like the following:

prog: ....
  ...

run: prog
  ./prog

Is there any way to pass arguments? So that

make run asdf --> ./prog asdf
make run the dog kicked the cat --> ./prog the dog kicked the cat

Thanks!

This question is related to makefile

The answer is


This question is almost three years old, but anyway...

If you're using GNU make, this is easy to do. The only problem is that make will interpret non-option arguments in the command line as targets. The solution is to turn them into do-nothing targets, so make won't complain:

# If the first argument is "run"...
ifeq (run,$(firstword $(MAKECMDGOALS)))
  # use the rest as arguments for "run"
  RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
  # ...and turn them into do-nothing targets
  $(eval $(RUN_ARGS):;@:)
endif

prog: # ...
    # ...

.PHONY: run
run : prog
    @echo prog $(RUN_ARGS)

Running this gives:

$ make run foo bar baz
prog foo bar baz

anon, run: ./prog looks a bit strange, as right part should be a target, so run: prog looks better.

I would suggest simply:

.PHONY: run

run:
        prog $(arg1)

and I would like to add, that arguments can be passed:

  1. as argument: make arg1="asdf" run
  2. or be defined as environment: arg1="asdf" make run

Here is my example. Note that I am writing under Windows 7, using mingw32-make.exe that comes with Dev-Cpp. (I have c:\Windows\System32\make.bat, so the command is still called "make".)

clean:
    $(RM) $(OBJ) $(BIN) 
    @echo off
    if "${backup}" NEQ "" ( mkdir ${backup} 2> nul && copy * ${backup} )

Usage for regular cleaning:

make clean

Usage for cleaning and creating a backup in mydir/:

make clean backup=mydir

Here's another solution that could help with some of these use cases:

test-%:
    $(PYTHON) run-tests.py $@

In other words, pick some prefix (test- in this case), and then pass the target name directly to the program/runner. I guess this is mostly useful if there is some runner script involved that can unwrap the target name into something useful for the underlying program.


Another trick I use is the -n flag, which tells make to do a dry run. For example,

$ make install -n 
# Outputs the string: helm install stable/airflow --name airflow -f values.yaml
$ eval $(make install -n) --dry-run --debug
# Runs: helm install stable/airflow --name airflow -f values.yaml --dry-run --debug

Not too proud of this, but I didn't want to pass in environment variables so I inverted the way to run a canned command:

run:
    @echo command-you-want

this will print the command you want to run, so just evaluate it in a subshell:

$(make run) args to my command

You can explicitly extract each n-th argument in the command line. To do this, you can use variable MAKECMDGOALS, it holds the list of command line arguments given to 'make', which it interprets as a list of targets. If you want to extract n-th argument, you can use that variable combined with the "word" function, for instance, if you want the second argument, you can store it in a variable as follows:

second_argument := $(word 2, $(MAKECMDGOALS) )

for standard make you can pass arguments by defining macros like this

make run arg1=asdf

then use them like this

run: ./prog $(arg1)
   etc

References for make Microsoft's NMake


I found a way to get the arguments with an equal sign (=)! The answer is especially an addition to @lesmana 's answer (as it is the most complete and explained one here), but it would be too big to write it as a comment. Again, I repeat his message: TL;DR don't try to do this!

I needed a way to treat my argument --xyz-enabled=false (since the default is true), which we all know by now that this is not a make target and thus not part of $(MAKECMDGOALS).

While looking into all variables of make by echoing the $(.VARIABLES) i got these interesting outputs:

[...] -*-command-variables-*- --xyz-enabled [...]

This allows us to go two ways: either getting all starting with a -- (if that applies to your case), or look into the GNU make specific (probably not intended for us to use) variable -*-command-variables-*-. ** See footer for additional options ** In my case this variable held:

--xyz-enabled=false

With this variable we can combine it with the already existing solution with $(MAKECMDGOALS) and thus by defining:

# the other technique to invalidate other targets is still required, see linked post
run:
    @echo ./prog $(-*-command-variables-*-) $(filter-out $@,$(MAKECMDGOALS))`

and using it with (explicitly mixing up order of arguments):

make run -- config --xyz-enabled=false over=9000 --foo=bar show  isit=alwaysreversed? --help

returned:

./prog isit=alwaysreversed? --foo=bar over=9000 --xyz-enabled=false config show --help

As you can see, we loose the total order of the args. The part with the "assignment"-args seem to have been reversed, the order of the "target"-args are kept. I placed the "assignment"-args in the beginning, hopefully your program doesn't care where the argument is placed.


Update: following make variables look promising as well:

MAKEFLAGS =  -- isit=alwaysreverse? --foo=bar over=9000 --xyz-enabled=false
MAKEOVERRIDES = isit=alwaysreverse? --foo=bar over=9000 --xyz-enabled=false

No. Looking at the syntax from the man page for GNU make

make [ -f makefile ] [ options ] ... [ targets ] ...

you can specify multiple targets, hence 'no' (at least no in the exact way you specified).


You can pass the variable to the Makefile like below:

run:
    @echo ./prog $$FOO

Usage:

$ make run FOO="the dog kicked the cat"
./prog the dog kicked the cat

or:

$ FOO="the dog kicked the cat" make run
./prog the dog kicked the cat

Alternatively use the solution provided by Beta:

run:
    @echo ./prog $(filter-out $@,$(MAKECMDGOALS))
%:
    @:

%: - rule which match any task name; @: - empty recipe = do nothing

Usage:

$ make run the dog kicked the cat
./prog the dog kicked the cat

TL;DR don't try to do this

$ make run arg

instead create script:

#! /bin/sh
# rebuild prog if necessary
make prog
# run prog with some arguments
./prog "$@"

and do this:

$ ./buildandrunprog.sh arg

answer to the stated question:

you can use a variable in the recipe

run: prog
    ./prog $(var)

then pass a variable assignment as an argument to make

$ make run var=arg

this will execute ./prog arg.

but beware of pitfalls. i will elaborate about the pitfalls of this method and other methods further down.


answer to the assumed intention behind the question:

the assumption: you want to run prog with some arguments but have it rebuild before running if necessary.

the answer: create a script which rebuilds if necessary then runs prog with args

#! /bin/sh
# rebuild prog if necessary
make prog
# run prog with some arguments
./prog "$@"

this script makes the intention very clear. it uses make to do what it is good for: building. it uses a shell script to do what it is good for: batch processing.

plus you can do whatever else you might need with the full flexibility and expressiveness of a shell script without all the caveats of a makefile.

also the calling syntax is now practically identical:

$ ./buildandrunprog.sh foo "bar baz"

compare to:

$ ./prog foo "bar baz"

contrast to

$ make run var="foo bar\ baz"

background:

make is not designed to pass arguments to a target. all arguments on the command line are interpreted either as a goal (a.k.a. target), as an option, or as a variable assignment.

so if you run this:

$ make run foo --wat var=arg

make will interpret run and foo as goals (targets) to update according to their recipes. --wat as an option for make. and var=arg as a variable assignment.

for more details see: https://www.gnu.org/software/make/manual/html_node/Goals.html#Goals

for the terminology see: https://www.gnu.org/software/make/manual/html_node/Rule-Introduction.html#Rule-Introduction


about the variable assignment method and why i recommend against it

$ make run var=arg

and the variable in the recipe

run: prog
    ./prog $(var)

this is the most "correct" and straightforward way to pass arguments to a recipe. but while it can be used to run a program with arguments it is certainly not designed to be used that way. see https://www.gnu.org/software/make/manual/html_node/Overriding.html#Overriding

in my opinion this has one big disadvantage: what you want to do is run prog with argument arg. but instead of writing:

$ ./prog arg

you are writing:

$ make run var=arg

this gets even more awkward when trying to pass multiple arguments or arguments containing spaces:

$ make run var="foo bar\ baz"
./prog foo bar\ baz
argcount: 2
arg: foo
arg: bar baz

compare to:

$ ./prog foo "bar baz"
argcount: 2
arg: foo
arg: bar baz

for the record this is what my prog looks like:

#! /bin/sh
echo "argcount: $#"
for arg in "$@"; do
  echo "arg: $arg"
done

also note that you should not put $(var) in quotes in the makefile:

run: prog
    ./prog "$(var)"

because then prog will always get just one argument:

$ make run var="foo bar\ baz"
./prog "foo bar\ baz"
argcount: 1
arg: foo bar\ baz

all this is why i recommend against this route.


for completeness here are some other methods to "pass arguments to make run".

method 1:

run: prog
    ./prog $(filter-out $@, $(MAKECMDGOALS))

%:
    @true

super short explanation: filter out current goal from list of goals. create catch all target (%) which does nothing to silently ignore the other goals.

method 2:

ifeq (run, $(firstword $(MAKECMDGOALS)))
  runargs := $(wordlist 2, $(words $(MAKECMDGOALS)), $(MAKECMDGOALS))
  $(eval $(runargs):;@true)
endif

run:
    ./prog $(runargs)

super short explanation: if the target is run then remove the first goal and create do nothing targets for the remaining goals using eval.

both will allow you to write something like this

$ make run arg1 arg2

for deeper explanation study the manual of make: https://www.gnu.org/software/make/manual/html_node/index.html

problems of method 1:

  • arguments that start with a dash will be interpreted by make and not passed as a goal.

    $ make run --foo --bar
    

    workaround

    $ make run -- --foo --bar
    
  • arguments with an equal sign will be interpreted by make and not passed

    $ make run foo=bar
    

    no workaround

  • arguments with spaces is awkward

    $ make run foo "bar\ baz"
    

    no workaround

  • if an argument happens to be run (equal to the target) it will also be removed

    $ make run foo bar run
    

    will run ./prog foo bar instead of ./prog foo bar run

    workaround possible with method 2

  • if an argument is a legitimate target it will also be run.

    $ make run foo bar clean
    

    will run ./prog foo bar clean but also the recipe for the target clean (assuming it exists).

    workaround possible with method 2

  • when you mistype a legitimate target it will be silently ignored because of the catch all target.

    $ make celan
    

    will just silently ignore celan.

    workaround is to make everything verbose. so you see what happens. but that creates a lot of noise for the legitimate output.

problems of method 2:

  • if an argument has same name as an existing target then make will print a warning that it is being overwritten.

    no workaround that i know of

  • arguments with an equal sign will still be interpreted by make and not passed

    no workaround

  • arguments with spaces is still awkward

    no workaround

  • arguments with space breaks eval trying to create do nothing targets.

    workaround: create the global catch all target doing nothing as above. with the problem as above that it will again silently ignore mistyped legitimate targets.

  • it uses eval to modify the makefile at runtime. how much worse can you go in terms of readability and debugability and the Principle of least astonishment.

    workaround: don't do this!!1 instead write a shell script that runs make and then runs prog.

i have only tested using gnu make. other makes may have different behaviour.


TL;DR don't try to do this

$ make run arg

instead create script:

#! /bin/sh
# rebuild prog if necessary
make prog
# run prog with some arguments
./prog "$@"

and do this:

$ ./buildandrunprog.sh arg