Subtract and Echo Difference of Variables in GNU Make

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP


Subtract and Echo Difference of Variables in GNU Make



I'm echoing the time before I run a task and the time that task ends:

test:
@echo $(shell date)
@JUNIT_REPORT_PATH=test/report.xml ./node_modules/.bin/mocha test/integration
@echo $(shell date)

Is there a way to store the dates in 2 variables and then show the elapsed time between them?
I assume it would look something like this:



test:
@echo $(shell date)
@JUNIT_REPORT_PATH=test/report.xml ./node_modules/.bin/mocha test/integration
@echo $(shell date)


test:
$begin = $(shell date)
<do stuff>
$end = $(shell date)
@echo $end - $begin



Any pointers to good documentation on Make would be appreciated too.



Thanks!




3 Answers
3



Short answer: what you want is possible:


test:
@begin=$$(date +%s);
<do stuff in same shell: do not forget the ";" and the "">;
end=$$(date +%s);
echo $$((end - begin))



Long answer: sorry for the long explanation, I think your question deserves it because there are many things to fix with your example solution.



I assume you use bash, your make invokes bash to execute your recipes and your date command comes from the GNU coreutils. If not, what follows may not work as expected. If you are under Mac OS X ask another question about how to use a recent version of bash and the GNU coreutils instead of the default.


date



Back to your question. Let's study your example solution:


test:
$begin = $(shell date)
<do stuff>
$end = $(shell date)
@echo $end - $begin



and fix step by step what needs to be fixed. There are a few things to consider and understand:



1) How to use make variables: make variables are assigned with name = value and they are expanded with $(name) or ${name}. Note that if you have a single-letter variable name A you can also expand it with $A. So, when make expands the first line of your example recipe, $begin = $(shell date) the result depends whether a make variable b exists and has a non-null value or not. make will thus expand the line as fooegin = <value> or egin = <value>. Let's fix this by removing the first $: begin = $(shell date).


name = value


$(name)


${name}


A


$A


$begin = $(shell date)


b


fooegin = <value>


egin = <value>


$


begin = $(shell date)



2) Where make variables can be assigned: not in recipes. So, in the recipe you wrote, begin and end are shell variables. And shell variables are assigned with name=value, not name = value. No spaces. In your example, when make will pass begin = <value> to the shell you will get a begin: command not found error. Let's fix this by removing the spaces: begin=$(shell date).


begin


end


name=value


name = value


begin = <value>


begin: command not found


begin=$(shell date)



3) How to use date: the default format is not suitable for arithmetic. Use date +%s to return the current date - time as a timestamp (seconds since epoch). Let's fix this with begin=$(shell date +%s).


date +%s


begin=$(shell date +%s)



4) How to use bash arithmetic expansion: $((end - begin)) computes the difference between shell variables end and begin. Let's fix this by replacing the last line of your recipe by @echo $((end - begin)). But wait! this will not work because make will try to expand it before passing it to the shell and, guess what, it will consider (end - begin as a make variable name and expand it as nothing because there is no such make variable. So make will pass


$((end - begin))


end


begin


@echo $((end - begin))


(end - begin


echo )



to the shell and you will get a syntax error. To fix this we must double the $ sign: @echo $$((end - begin)). Note that make sometimes expands more than one time. In these cases you need more $ signs: @echo $$$$((end - begin)) for a double make expansion. But it is not your case.


$


@echo $$((end - begin))


$


@echo $$$$((end - begin))



5) When make expands its variables and functions: make expands its variables and functions when it needs their value and it is not when you think. It is before executing the recipes. So, begin and end, in your example, will have the same value, whatever time <do stuff> takes and it will be the date and time of the instant when make expanded $(shell date +%s). Try:


begin


end


<do stuff>


$(shell date +%s)


test:
@echo $(shell date +%s)
@sleep 2
@echo $(shell date +%s)



and see that the two timestamps are the same. Let's fix this by not using $(shell): begin=$$(date +%s) (note the $$). make will expand it as begin=$(date +%s) and pass it to the shell. The shell will do what you think and the begin shell variable will be assigned the current timestamp at the instant of shell invocation.


$(shell)


begin=$$(date +%s)


$$


begin=$(date +%s)


begin



6) How make processes the recipes: each line, after expansion, is passed to a different shell. Yes, in your example you would have at least 4 different shell invocations. More if <do stuff> hides several lines. The problem here, is that our begin and end shell variables do not exist for the shell that will execute the last line and you will get another shell error. Let's fix this by putting all shell commands on the same line, separated by the ; shell separator:


<do stuff>


begin


end


;


test:
@begin=$$(date +%s); sleep 2; end=$$(date +%s); echo $$((end - begin))



Bingo! It starts working. Note that the initial @ disables the echoing for the whole list of shell commands.


@



For very long recipes this is not very convenient, however. Don't worry, you can break the lines but the last character must be a to tell make to ignore the line break:



target:
@<do this>; <do that>; baz=1;
<and also this>;
if <condition>; then
<do this>;
else
<do that>;
fi;
<and this>;
echo $$baz



The preceding recipe is executed in a single shell invocation. The shell variable baz is the same variable from beginning to end. The initial @ still disables the echoing for the whole list of shell commands. Try:


baz


@


test:
@begin=$$(date +%s);
sleep 2;
end=$$(date +%s);
echo $$((end - begin))



and see that it works like the single line version. Warning: the must really just precede the line break. It is very easy to add a space or two after the . And if you do so, you will get shell errors. The worst of all, almost impossible to understand and debug. So, if you have strange errors with this kind of recipes, search the spaces at the end of lines. And if you know how to do this, tell your editor to highlight blanks at the end of lines in makefiles.





We are almost done. All in all,


test:
@begin=$$(date +%s);
<do stuff in same shell: do not forget the ";" and the "">;
end=$$(date +%s);
echo $$((end - begin))



should do what you want. make first notes the initial @ and disables the echoing, then it expands the recipe as one single list of commands:


@


begin=$(date +%s); <do stuff>; end=$(date +%s); echo $((end - begin))



and passes it to the shell in a unique invocation.



A last but important remark: because there is one single shell invocation it is also much faster. If you have complex makefiles doing complex things it can make a significant difference. Always use this, if you can, and speed up your processing.





Wow. Thank you for such a detailed example. Great stuff in here.
– spinlock
Sep 28 '15 at 16:26



In case you just want to get the time elapsed, I'd recommend /usr/bin/time for that (not the shell version of time), e.g.


/usr/bin/time


time


/usr/bin/time -f "%E real,%U user,%S sys" g++ something



Sadly, doing multiple things under one timer gets ugly:


/usr/bin/time -f "%E real,%U user,%S sys" sh -c 'g++ a; g++ b'



I'd just wanted to share /usr/bin/time here. Should work nice for simple recipes.





great answer. I selected the one above because it was more specific to make and explained the issues I was having with variables, etc.... But, your answer rocks and time is great to know about. I'm on a mac so I did have to install gnu-time with homebrew. Then, I just run gtime (don't have to worry about colliding with shell time).
– spinlock
Sep 28 '15 at 16:28


time


gnu-time


gtime


time



Arithmetic operations can be done in makefile as follows.


NUMBER1 := 10
NUMBER2 := 5

#Addition
ADD := $(shell echo ${NUMBER1}+${NUMBER2} | bc)

#Subtraction
SUBTRACT := $(shell echo ${NUMBER1}-${NUMBER2} | bc)

#Multiplication
MULTIPLY := $(shell echo ${NUMBER1}*${NUMBER2} | bc)

#Division
DIVIDE := $(shell echo ${NUMBER1}/${NUMBER2} | bc)

#Division (Floating Point)
DIVIDEF := $(shell echo "scale=3; ${NUMBER2}/${NUMBER1}" | bc)

#Modulo
MODULO := $(shell echo ${NUMBER1}%${NUMBER2} | bc)

#Comparison Greater Than
COMPARISON1 := $(shell echo ${NUMBER1}>=${NUMBER2} | bc)

#Comparison Smaller Than
COMPARISON2 := $(shell echo ${NUMBER2}<=${NUMBER2} | bc)

all:
@echo Addition ${ADD}
@echo Subtraction ${SUBTRACT}
@echo Multiplication ${MULTIPLY}
@echo Division ${DIVIDE}
@echo Division - Floating Point ${DIVIDEF}
@echo Modulo ${MODULO}
@echo Comparison Greater Than ${COMPARISON1}
@echo Comparison Smaller Than ${COMPARISON2}



Courtesy: Makefile Tricks: Arithmetic – Addition, Subtraction, Multiplication, Division, Modulo, Comparison






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Arduino Mega cannot recieve any sketches, stk500_recv() programmer is not responding

Visual Studio Code: How to configure includePath for better IntelliSense results

Future solutions