spinlock
spinlock

Reputation: 3957

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:
    $begin = $(shell date)
    <do stuff>
    $end = $(shell date)
    @echo $end - $begin

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

Thanks!

Upvotes: 1

Views: 1271

Answers (3)

Nikhil Augustine
Nikhil Augustine

Reputation: 197

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

Upvotes: 0

m8mble
m8mble

Reputation: 1545

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

Upvotes: 1

Renaud Pacalet
Renaud Pacalet

Reputation: 29280

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.

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

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

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

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

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.

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:

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.

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:

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:

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.

Upvotes: 4

Related Questions