If I have the following bash command:
for i in ./ x ; do ls $i ; done && echo OK
"ls ./" is executed, and then "ls x", which fails (x is missing) and OK is not printed.
If
for i in x ./ ; do ls $i ; done && echo OK
then even though "ls x" fails, because the last statement in the for loop succeeded, then OK is printed. This is a problem when using shell for loops in makefiles:
x:
for i in $(LIST) ; do \
cmd $$i ;\
done
How can I make make fail if any of the individual executions of cmd fails?
Use the break command to terminate the loop when a command fails
x:
for i in $(LIST) ; do \
cmd $$i || break ;\
done
That won't make the makefile abort, though. You could instead use exit with a non-zero code:
x:
for i in $(LIST) ; do \
cmd $$i || exit 1 ;\
done
After executing the command, check for return value of that command using $?, as its make file you need to use double $. If its non zero, then exit with failure.
x:
set -e
for i in $(LIST); do \
cmd $$i; \
[[ $$? != 0 ]] && exit -1; \
echo 'done'; \
done
Related
This question already has answers here:
Why should there be spaces around '[' and ']' in Bash?
(5 answers)
Closed 7 months ago.
I need to add shell if-else statement to my Makefile, but the if expression always evaluates to false.
For example the next code:
if [1 -eq 1]; then echo "yes"; else echo "no"
prints "no"
The only code that evaluated to true was:
if true; then echo "yes"; else echo "no"
Why all expressions in the code (except for "true") evaluates to false? :(
I would really appreciate any help
** Please note - the statements work correctly when run from Shell
The code snippet from the original Makefile:
SIMULATION_RUN_CMD = rm -rf $(TEST_DIR)/* && mkdir -p $(TEST_DIR) && cd $(TEST_DIR) && (cp -rf $(VIVADO_PROJ)/$(PROJECT)/export_for_sim/$(SIMULATOR)/{*.mem,.mif,design.dat,nocattrs.dat,cpm_data_sim.cdo} $(TEST_DIR) || true) && \
ln -sf $(TEST_DIR)/simulation.log $(RUN_DIR)/simulation.log && \
(timeout $(SIM_TIMEOUT) ${SIM_DIR}/simv +UVM_TESTNAME=$(UVM_TESTNAME) $(SIM_FLAGS) -l $(TEST_DIR)/simulation.log -do $(DO_FILE) ; \
if [1 -eq 1]; then echo "if statement yes " >> $(TEST_DIR)/simulation.log; else echo "if statement no " >> $(TEST_DIR)/simulation.log; fi \
|| true) && \
$(MODEL_POST_SIM_ACIONS)
$(SIMULATION_RUN_TAR):
#echo -e "Make Command: $(SIMULATION_RUN_CMD)" $(PRINT_OUTPUT)
($(SIMULATION_RUN_CMD)) $(PRINT_OUTPUT)
First, you have a syntax error in your command. If you type that exactly into bash you'll get an error:
[[1: command not found
You need spaces after the [[ and before the ]] tokens:
if [[ 1 -eq 1 ]]; then echo "yes"; else echo "no"
Second, the reason it doesn't work when run from make is that make doesn't invoke bash. Make invokes the POSIX standard shell /bin/sh. If you do this you'll see the same behavior you get with make:
$ /bin/sh -c 'if [[ 1 -eq 1 ]]; then echo yes; else echo no; fi'
/bin/sh: 1: [[: not found
no
The [[ operator is a bash-specific feature. If you want to write this using POSIX features you should use:
$ /bin/sh -c 'if [ 1 -eq 1 ]; then echo yes; else echo no; fi'
yes
If you really want make to invoke bash as its shell instead of sh, add this to your makefile:
SHELL := /bin/bash
Of course then your makefile will not work on any system that doesn't have /bin/bash available.
ETA
After seeing the very much more complicated, but still not complete, code you added, I will say the following:
As I said above, you have an error in your script. if [1 -eq 1] is completely illegal. You must have spaces after [ and before ]. Again, if you run this yourself at the shell prompt you will get the same failure. It has nothing to do with make.
Because of this error, the if-statement will ALWAYS fail and so this will ALWAYS run the "else" command and print "no".
You say you don't see any error message. I can't explain that, except that you run this recipe this way:
($(SIMULATION_RUN_CMD)) $(PRINT_OUTPUT)
You don't tell use what the value of the PRINT_OUTPUT variable is, so I can only assume that it throws away stderr into the bit-bucket (or possibly, both stdout and stderr). If you didn't do that, so you could see the output, you'd see the error message being printed. Or maybe that redirects to a log file in which case, you can look there for the message.
I'm trying to run a shell command (currently either sh or bash) which connects to a database. Because the database is still 'warming up', the command fails.
So I was trying to do a loop (let's say ... 100 tries) and each time the command fails, wait 1 second and retry.
If there's an error, this is the start of the string that is dumped to stdout: Sqlcmd: Error: <snipped>
Here's what I've been trying:
for i in $(seq 1 100)
do
X='/opt/mssql-tools/bin/sqlcmd -E -S localhost -Q "<some sql statement> "'
if [[ $X == Sqlcmd: Error:* ]]
echo "."
then
break
fi
done
It's not working as I figure out the string comparison stuff with shell/bash ... but was more making sure if I was on the right track etc.
You could try something like:
while true ; do
if Sqlcmd xxx xxx xxx ; then break ; fi
# or:
Sqlcmd xx xxx xxx && break
sleep 1
done
You can also add a counter:
for ((n=100;n>0;n--)) ; do
Sqlcmd xxx xxx xxx
if [[ $? == 0 ]] ; then
break
fi
sleep 1
done
[[ $n == 0 ]] && echo Timeout && exit 1
I'm showing two different ways of testing the return value here, but the first one is preferred (if cmd ; then ... ; fi).
$? is the return value from the last command, which is 0 when it completed successfully. If it returns 0 even in case of error (which can happen for malformed programs), you can test the output with grep:
Sqlcmd xxx xxx 2>&1 | grep <error pattern> > /dev/null
if [[ $? != 0 ]] ; then break ; fi
Here we test $? != 0 because grep will return 0 when the error pattern has been found.
If you want to get the output result into a variable, run the command with X=$(Sqlcmd xxx xxx). Then you can use bash string comparison:
X=$(Sqlcmd xxx xxx)
if [[ "$X" =~ .*error.* ]] ; then
<handle error here>
fi
Note bash can match regexp, which makes it really handy at checking error types.
You can also use a switch/case construct:
case "$X" in
*Error:*) echo " Error detected " ;;
*) break ;;
esac
(Note the double ;;)
I ended up learning all the clues from #matthieu's post. This is what I ended up doing:
for i in $(seq 1 30)
do
/opt/mssql-tools/bin/sqlcmd -U sa -P <snip> -S localhost -Q "USE Master" 2>&1
if [[ $? != 0 ]]
then
# Failed
echo "."
sleep 1s
else
# worked!
break
fi
done
breakdown for those learning (like me)
execute a sql query using the sqlcmd command. Any errors via stderr (that's the 2 in 2>&1) will be redirected to the console stdout (that's the $1). REF: 2>&1 shell idiom.
the result status code is sent to $? (REF: what is bash dollar questionmark ?)
if it failed(any value that is NOT a zero), then sleep 1 sec and we'll try. Only re-try 30 times, though.
if we worked (value is zero), then stop trying and go on....
So there we have it! shell/bash shell 101 stuff. good luck!
%/all:
if [ -f $(#D)/src/Makefile ]; then \
$(MAKE) -C $(#D); \
fi
If the inner make fails, the outer make continues, presumably because the implicit exit status of the 'if' command is 0. Is there a way around this?
This cannot be a real example. The shell will exit with the result of the last command executed, which if the if-statement succeeds will be the exit code of make, which is what you want. So obviously in your real code, you must be doing some other command between the make and the end. You can keep a copy of the result in a variable and use that as the exit:
if [ -f $(#D)/src/Makefile ]; then \
$(MAKE) -C $(#D); \
r=$$?; \
...do other stuff...; \
exit $$r; \
fi
Somehow I couldn't reproduce your problem but I suppose the following should work for you :
%/all:
if [ -f $(#D)/src/Makefile ]; then \
$(MAKE) -C $(#D) || (echo "make failure: $$?"; exit 1) \
fi
I'm trying to do this ..... and this is how my Makefile look like
.PHONY: run
SHELL := /bin/tcsh
run:
md5sum -c md; \
if ($$?==0) then \
echo "PASS" \
else \
echo "FAIL" \
endif
But i got this error.
if: Badly formed number.
make: *** [run] Error 1
Is what I'm doing correct? Or is there a better way of doing that in a Makefile?
Basically, you should simply not, ever, use csh (or tcsh) for writing makefile rules. Write the rule using POSIX shell:
.PHONY: run
run:
md5sum -c md; \
if [ $$? -eq 0 ]; then \
echo "PASS"; \
else \
echo "FAIL"; \
fi
I have to compare a file with 3 different golden files using diff.
I need to exit the script with exit 0 if test file is the same as any of the three golden files.
I tried the following:
#!/bin/sh
one=`diff -q NEW_GOLDEN_OUTPUT_ASYNC_1 /tmp/tmp_last_lines.log`
two=`diff -q NEW_GOLDEN_OUTPUT_ASYNC_2 /tmp/tmp_last_lines.log`
three=`diff -q NEW_GOLDEN_OUTPUT_ASYNC_3 /tmp/tmp_last_lines.log`
if [[ $one || $two || $three ]]; then
exit 0
else
exit 1
fi
But it returns exit 0 in all cases. I'm using /bin/ksh shell. Any suggestions?
Your code looks at the output of diff but you should look at the exit code. Try this instead:
#!/bin/sh
diff -q NEW_GOLDEN_OUTPUT_ASYNC_1 /tmp/tmp_last_lines.log && \
diff -q NEW_GOLDEN_OUTPUT_ASYNC_2 /tmp/tmp_last_lines.log && \
diff -q NEW_GOLDEN_OUTPUT_ASYNC_3 /tmp/tmp_last_lines.log
&& will only execute the next command if the previous one succeeded.
Alternatively, use set -e (Exit immediately if a command exits with a non-zero status.):
#!/bin/sh
set -e
diff -q NEW_GOLDEN_OUTPUT_ASYNC_1 /tmp/tmp_last_lines.log
diff -q NEW_GOLDEN_OUTPUT_ASYNC_2 /tmp/tmp_last_lines.log
diff -q NEW_GOLDEN_OUTPUT_ASYNC_3 /tmp/tmp_last_lines.log