Here is a simplified Makefile:
all:
#for (( i = 0; i < 5; ++i )); do \
var="$$var $$i"; \
echo $$var; \
done
#echo $$var
I suppose the value of "var" is "0 1 2 3 4", but the output is:
0
0 1
0 1 2
0 1 2 3
0 1 2 3 4
<--- NOTHING!!!
As you can see the last echo is "NOTHING". What is wrong?
From here:
When it is time to execute recipes to update a target, they are executed by invoking a new subshell for each line of the recipe...
Please note: this implies that setting shell variables and invoking shell commands such as cd that set a context local to each process will not affect the following lines in the recipe. If you want to use cd to affect the next statement, put both statements in a single recipe line. Then make will invoke one shell to run the entire line, and the shell will execute the statements in sequence.
Try the following:
all:
#for (( i = 0; i < 5; ++i )); do \
var="$$var $$i"; \
echo $$var; \
done; \
echo $$var
Related
In my Makefile I want to check if multiple environments variables are set.
But I don't want to write multiple ifndef for each one. I just want an array of variables to make it reusable.
check-variables:
for var in var1 var2 var3 ; do \
if [ -z "$$var" ] ; then \
echo "$$var is not set"; exit 1; \
fi \
done
But it's not working...
This isn't a make issue, it's a shell issue. If you ran that script at your shell prompt (changing $$ back to $ as make will do, of course) it wouldn't work either. Until you can get the command to work at your shell prompt, you can't get it to work in a makefile.
The shell command would be:
for var in var1 var2 var3 ; do \
if [ -z "$var" ] ; then \
echo "$var is not set"; exit 1; \
fi \
done
You can see why this doesn't do what you want: you're checking the shell variable var every time. What you're trying to do is check the value of the variable which is named by the value of $var. To do this you need eval (the shell's eval, not make's eval function):
for var in var1 var2 var3 ; do \
eval test -n \"\$$var\" \
|| { echo "$var is not set"; exit 1; }; \
done
You should discover that the above will work in the shell, then you need to put it back into the makefile (and double all the $).
❯ make --version
GNU Make 3.81
❯ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
How can I pass a variable from inside a for loop to $(shell)? I can access the var outside of $(shell) but I can't figure out how to pass it in:
A_LIST:= one two
.PHONY: loop
loop:
#for iii in $(A_LIST) ; do \
echo inside recipe loop with sh command: $$iii ; \
export SAVED_OUTPUT=$(shell echo $$iii) ; \
echo $$SAVED_OUTPUT ; \
done
This is the output I get:
inside recipe loop with sh command: one
<blank line here>
inside recipe loop with sh command: two
<blank line here>
The last line in the loop echo $$SAVED_OUTPUT should output one and two because it is echoing the var and storing it in another var. But it is a blank line. I suspect it's because it's looking for an env var $iii but that doesn't exist- so how to I pass the value of iii into the shell?
Here is a bad way of doing this I don't like. I don't want to have to write a local file just to access a variable like this:
.PHONY: loop
loop:
#for iii in $(A_LIST) ; do \
echo inside recipe loop with sh command: $$iii ; \
echo $$iii > scratch ; \
export SAVED_OUTPUT=$(shell echo $$(cat scratch)) ; \
echo $$SAVED_OUTPUT ; \
done
The for loop is already being executed by a shell - in this case, there's no reason to bring $(shell ...) into it too. Just use normal $() shell command substitution syntax (Doubling up the $ to make make happy, like with the variable names):
A_LIST:= one two
.PHONY: loop
loop:
#for iii in $(A_LIST) ; do \
echo "inside recipe loop with sh command: $$iii" ; \
SAVED_OUTPUT="$$(somecommand "$$iii")" ; \
echo "$$SAVED_OUTPUT" ; \
done
I want to run a program with increasing input sizes, so I introduced a run target that contains a loop.
run:
input_size = 1000 ; while [[ $$input_size -le 10000 ]] ; do \
echo $$input_size ; \
./main.out $$input_size ; \
((input_size = input_size + 1000)) ; \
done
I get the following error:
/bin/sh: input_size: command not found
after which, the loop works as expected.
Your makefile has several issues, if we fix your version, it should be:
run:
input_size=1000 ; while [ $$input_size -le 10000 ] ; do \
echo $$input_size ; \
./main_out $$input_size ; \
input_size=$$((input_size + 1000)) ; \
done
An easier method for generating sequences of numbers is the use of seq:
run:
for input_size in $$(seq 1000 1000 10000); do \
echo $$input_size ; \
./main_out $$input_size ; \
done
In bash there must be no space around the assignment symbol (unlike make):
input_size=1000
Alternatively, you can use bash arithmetic expansion, which is more relaxed about whitespace:
SHELL := /bin/bash
run:
for (( input_size = 1000; input_size < 10000; input_size += 1000 )); do \
echo $$input_size; \
done
Just to be very clear about the problem you have:
The problem is that make doesn't run recipes using whatever shell the user has: that would be a disaster for repeatable builds. make always (by the POSIX standard) runs recipes in the /bin/sh shell which is a POSIX shell.
Your recipes are using enhanced features of the bash shell, which are not available in POSIX shell. You need to either rewrite your recipes to use POSIX shell features, or else explicitly request that make use bash to run your recipes instead of the POSIX shell (of course this means your makefile is not portable to any system where bash is not available, if this is a concern).
You can do the latter by adding this to your makefile:
SHELL := /bin/bash
So I'm trying to hack one of my makefiles to be simpler (simpler, as if, not defining a lot of rules how to transform subdirectory into .deb).
build-if-need-status-vars:
#if [ ! -f debs/1.deb ]; then \
$(eval STATUS_REBUILD=1) \
echo "component: file not found: 1"; exit;\
else \
if [ $(shell find sources/ -newer debs/1.deb 2>/dev/null | wc -l) -gt 0 ]; then \
$(eval STATUS_REBUILD=1) echo "component: newer files exists: 1"; exit;\
else \
$(eval STATUS_REBUILD=0) echo "component: no newer files: 0"; \
fi;\
fi
#echo "status $(STATUS_REBUILD)"
actual-target: build-if-need-status-vars
ifeq ($(STATUS_REBUILD), 1)
#echo first status: 1
else
#echo second status: 0
#echo different action
endif
all: actual-target
.PHONY: actual-target
Test with:
mkdir -p test/{sources,debs}; touch test/debs/1.deb; sleep 2; touch test/sources/1.src; (create makefile there and run)
Result:
component: file not found: 1
status 0
second status: 0
Regardless of what conditional block is executed, STATUS_REBUILD will always be 0 (last evaluated value), try it: touch test/debs/1.deb
So it seems that last $(eval) is always used.. How to avoid this behaviour and keep the correct assigned value (from first match in build-if-need-status-var)?
$(eval) is a make-level function. It is expanded in your recipe during recipe the recipe expansion stage.
The contents of a recipe are expanded in the second phase of makefile parsing (discussed briefly in the manual here).
I believe, but cannot say for sure (without testing), that recipes are not expanded until they are about to be run (but for the purposes here that doesn't change anything either way).
So your problem here is that all the $(eval) calls are expanded by the time make goes to run your shell script so you always see the last value in effect when the last line is run.
That all being said you don't actually need a make-level variable here. Your recipe is already only two shell executions.
You can simply include the last line in the same execution as the first (split) line and use a shell variable.
build-if-need-status-vars:
#if [ ! -f debs/1.deb ]; then \
STATUS_REBUILD=1; \
echo "component: file not found"; \
else \
if [ $(shell find sources/ -newer debs/1.deb 2>/dev/null | wc -l) -gt 0 ]; then \
STATUS_REBUILD=1; echo "component: newer files exists"; \
else \
STATUS_REBUILD=0; echo "component: no newer files"; \
fi;\
fi; \
echo "status $$STATUS_REBUILD"
Note that I needed to remove the exit pieces to make this work. If those are necessary in the real makefile (because this is a stripped down sample) then you can keep them by wrapping the if in a sub-shell and/or by rewriting the recipe.
Here's my code:
la := a b c d
lb := 1 2 3 4
define test =
First line $$vara
Second line $$varb
endef
export test
#for vara in $(la); do \
for varb in $(lb); do \
echo $$vara $$varb; \
echo "$$test"; \
done \
done
Later this output will go in a file I'm creating using cat.
I'm expecting something like
a 1
First line a
Second line 1
a 2
First line a
Second line 2
...
But instead I'm getting
a 1
$vara $varb
a 2
$vara $varb
What am I doing wrong?
EDIT: SOLUTION
I used as a base #Beta's answer and changed it a bit to allow it to save the result in a file and send it over SSH to another terminal.
define test
[program:$$s-$$c]\nString 1\nString 2\n...\nThe End.
endef
export test
testmake:
#for s in $(serv); do \
for c in $(cust); do \
ssh user#host "mkdir -p /var/log/$$s/$$c"; \
echo "$(call test)" | ssh user#host "cat > /test.txt"; \
done \
done
This worked for me in GNU Make 3.81 (but there are some fussy little differences between versions, so you may have to tweak it):
define test
echo First line $$vara; \
echo Second line $$varb
endef
export test
some_rule:
#for vara in $(la); do \
for varb in $(lb); do \
echo $$vara $$varb; \
$(call test); \
done \
done
Note that this has the loops in a recipe -- which is what I inferred you were doing. If you want the loops to run outside a rule, the solution will look quite different.