why ‘&&’ disables errexit(set -e)? - bash

consider script here:
set -e
make && make install
echo "SHOULD NOT BE HERE"
I expect that if make fails, the script will be aborted, but it's not:
make: *** No targets specified and no makefile found. Stop.
SHOULD NOT BE HERE
But, if I changed it like this:
set -e
make
make install
echo "SHOULD NOT BE HERE"
It works as expected:
make: *** No targets specified and no makefile found. Stop.
Why this happens?
Due to make && make install is commonly used in my build script, how should I use it correctly?
And please DO NOT link this question to Using set -e / set +e in bash with functions, it's not the same question.

Quoting from the answer to the question you linked:
Quoting sh(1) from FreeBSD, which explains this better than bash's man page:
-e errexit
Exit immediately if any untested command fails in non-interactive
mode. The exit status of a command is considered to be explicitly
tested if the command is part of the list used to control an if,
elif, while, or until; if the command is the left hand operand of
an “&&” or “||” operator; or if the command is a pipeline preceded
by the ! operator. If a shell function is executed and its exit
status is explicitly tested, all commands of the function are con‐
sidered to be tested as well.
errexit exits only if an untested command fails. Using && (or ||) will means that bash considers the command to the left of && to be explicitly tested (which in turn means that it will not be handled by errexit).
See also here (specifically the part about list constructs).
As far a I know, there is no way to achieve what you would like by setting bash options.

Related

How do I write an if else statement in a makefile with the zsh shell?

I am writing a makefile, and my shell is zsh. All is operating well until I hit an if statement. I've tried using the traditional ifeq makefile statement, but that doesn't appear to work when using zsh. So, I've tried just using zsh if statements, like the following:
if [[ "$(BUILD_ENV)" == "DEV" ]]; then
#echo dev environment
else
#echo not dev environment
fi
Every time I run it, I get the following error:
...
if [[ "DEV" == "DEV" ]]; then
zsh:1: parse error near `then'
make: *** [build] Error 1
I've tried removing the semicolon (;), putting then on a new line (like in bash syntax), but to no avail.
How can I properly perform an if else statement in a makefile, using zsh as the shell?
It's irrelevant what shell you use (consider the insanity if make always used the shell of the user who invoked it... no makefile could ever be portable between two people!)
make always uses /bin/sh. If you want to use some different shell you must explicitly set it in your makefile by adding:
SHELL := /bin/zsh
However, I don't recommend this because not every system will have zsh installed.
You should simply write your recipes to use portable POSIX syntax rather than using zsh extensions. In this case, that means using [..] instead of [[...]] and using = for equality instead of ==.
Also, remember that each logical line of a makefile is invoked in a separate shell so if you want to write an if-statement you have to use backslashes to ensure all the physical lines are combined into a single logical line:
mytarget:
#if [ "$(BUILD_ENV)" = "DEV" ]; then \
echo dev environment; \
else \
echo not dev environment; \
fi

Abort issue when error/exception occurs?

I have a bash script which calls multiple scripts (bash & python) from some directories.
I would like to get it aborted when any of the script throws an error/exception.
#!/bin/bash
/usr/bin/test.sh /usr/1/sample.sh /usr/2/temp.py
exit 0
Any suggestion on how to achieve this ?
FYI : I'm a beginner in bash scripting.
You can put set -e at the top of the script:
-e errexit If not interactive, exit immediately if any
untested command fails. The exit status of a com‐
mand is considered to be explicitly tested if the
command is used to control an if, elif, while, or
until; or if the command is the left hand operand
of an “&&” or “||” operator.
This will only work if one of your commands exit with an exit code of non-zero on failure. Well-behaved programs should always exit with 0 only on success, and if yours don't, you probably want to fix that.
I'm not entirely sure what you expect this to do:
/usr/bin/test.sh /usr/1/sample.sh /usr/2/temp.py
Since this will run one command (/usr/bin/test.sh) with two arguments, you probably want to put them on separate lines.

Why doesn't this Bash script error out?

Here's my Bash script:
#!/bin/bash -e
if [ == "" ]; then
echo "BAD"
exit 1
fi
echo "OK"
And here's the output:
./test.sh: line 3: [: ==: unary operator expected
OK
The return code is 0.
There's an obvious syntax error at line 3. Rather than raise the syntax error and refuse to run the script, somehow the script just runs and reports the syntax error at run time. The -e flag hasn't protected me from this - apparently a syntax error in an if statement constitutes a false condition rather than a reason to immediately exit the program. BUT, somehow Bash has parsed that whole if ... fi block, so after ignoring the bad line, execution somehow resumes not at the next syntactically correct line but after the end of the block?
I have two questions:
What is going on?
How can I protect myself from this behaviour in future?
if runs the command [, and just examines its return code. Bash doesn't know nor care about the syntax for the [ command.
You can put some other command there, and Bash still won't know anything about its particular syntax.
Two things come to mind:
Using [[ instead of [: Bash does know and care about its syntax.
Using ShellCheck1; online, manually or within your favourite editor.
Both if and -e deal with exit codes: If it's non-zero if won't let you into the then block, and -e will exit. You can't really have both those behaviours at once. (Well, it seems [ exits with different codes for false results (1) and syntax errors (2), so it might be possible to ‘detect’ syntax errors.)
1Or some other tool, but that's the only one of which I know. Suggestions welcome.
You don't have a shell syntax error here.
You have an error in the arguments to the [ command/builtin.
The reason set -e doesn't help here is because that's explicitly not what it is supposed to do. set -e would become entirely useless if you couldn't have if statements in your code with it on. Just think about that.
If you look in the POSIX spec for what the -e/errexit flag does you see this description:
-e
When this option is on, when any command fails (for any of the reasons listed in Consequences of Shell Errors or by returning an exit status greater than zero), the shell immediately shall exit with the following exceptions:
The failure of any individual command in a multi-command pipeline shall not cause the shell to exit. Only the failure of the pipeline itself shall be considered.
The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.
If the exit status of a compound command other than a subshell command was the result of a failure while -e was being ignored, then -e shall not apply to this command.
This requirement applies to the shell environment and each subshell environment separately. For example, in:
set -e; (false; echo one) | cat; echo two
See point two there? That's your situation.
The reason the shell continues to execute is back to the "not a shell syntax error". You have a command error. The [ command/builtin attempted to parse its arguments and failed. It then returned an error return code. The if caught that, skipped its body and returned true (as per documented behavior of if when no conditions return true). So the shell script continued normally.
As I indicated in my comment, however, if you had used [[ (which is a bash-ism and a language construct) then your script would have had a syntax error and would have exited immediately on that line (at least in my tests).
From the bash man page
-e
Exit immediately if a pipeline (which may consist of a single simple command), a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces (see SHELL GRAMMAR above) exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or ││ list except the command following the final && or ││, any command in a pipeline but the last, or if the command's return value is being inverted with !. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.
Emphasis mine.
First, that isn't a syntax error. You are simply providing bad arguments to the [ command.
Second, the exit status of a command in the list following the if keyword are ignored for the purpose of the -e option.

Execute a line in Bash without aborting if it fails?

Is there a generic way in a bash script to "try" something but continue if it fails? The analogue in other languages would be wrapping it in a try/catch and ignoring the exception.
Specifically I am trying to source an optional satellite script file:
. $OPTIONAL_PATH
But when executing this, if $OPTIONAL_PATH doesn't exist, the whole script screeches to a halt.
I realize I could check to see if the file exists before sourcing it, but I'm curious if there is a generic reusable mechanism I can use that will ignore the error without halting.
Update: Apparently this is not normal behavior. I'm not sure why this is happening. I'm not explicitly calling set -e anywhere ($- is hB), yet it halts on the error. Here is the output I see:
./script.sh: line 36: projects/mobile.sh: No such file or directory
I added an echo "test" immediately after the source line, but it never prints, so it's not anything after that line that is exiting. I am running Mac OS 10.9.
Update 2: Nevermind, it was indeed shebanged as #!/bin/sh instead of #!/bin/bash. Thanks for the informative answer, Kaz.
Failed commands do not abort the script unless you explicitly configure that mode with set -e.
With regard to Bash's dot command, things are tricky. If we invoke bash as /bin/sh then it bails the script if the . command does not find the file. If we invoke bash as /bin/bash then it doesn't fail!
$ cat source.sh
#!/bin/sh
. nonexistent
echo here
$ ./source.sh
./source.sh: 3: .: nonexistent: not found
$ ed source.sh
35
1s/sh/bash/
wq
37
$ ./source.sh
./source.sh: line 3: nonexistent: No such file or directory
here
It does respond to set -e; if we have #!/bin/bash, and use set -e, then the echo is not reached. So one solution is to invoke bash this way.
If you want to keep the script maximally portable, it looks like you have to do the test.
The behavior of the dot command aborting the script is required by POSIX. Search for the "dot" keyword here. Quote:
If no readable file is found, a non-interactive shell shall abort; an interactive shell shall write a diagnostic message to standard error, but this condition shall not be considered a syntax error.
Arguably, this is the right thing to do, because dot is used for including pieces of the script. How can the script continue when a whole chunk of it has not been found?
Otherwise arguably, this is braindamaged behavior inconsistent with the treatment of other commands, and so Bash makes it consistent in its non-POSIX-conforming mode. If programmers want a command to fail, they can use set -e.
I tend to agree with Bash. The POSIX behavior is actually more broken than initially meets the eye, because this also doesn't work the way you want:
if . nonexistent ; then
echo loaded
fi
Even if the command is tested, it still aborts the script when it bails.
Thank GNU-deness we have alternative utilities, with source code.
You have several options:
Make sure set -e wasn't used, or turn it off with set +e. Your bash script should not exit by default simply because the . command failed.
Test that the file exists prior to sourcing.
[ -f "$OPTIONAL_PATH" ] && . "$OPTIONAL_PATH"
This option is complicated by the fact that if $OPTIONAL_PATH does not contain
any slashes, . will still try to find the file in your path.
If you want to keep set -e on, "hide" the failure like this:
. "$OPTIONAL_PATH" || true
Even if the source fails, the exit status of the command list as a whole will be 0, due to the || true.
(Much of this is covered [better] by Kaz's answer, especially the references to the POSIX standard, but I wasn't sure when or if he would undelete his answer.)
This is not the default behavior. Did you set -e or use #!/bin/bash -e anywhere in your script, to make it automatically exit on failure?
If so, you can use
. $OPTIONAL_PATH || true
to continue anyways.

What do #, - and + do as prefixes to recipe lines in Make?

In the GNU Makefile manual, it mentions these prefixes.
If .ONESHELL is provided, then only the first line of the recipe will be checked for the special prefix characters (‘#’, ‘-’, and ‘+’).
What do these prefixes do, and where are they mentioned?
They control the behaviour of make for the tagged command lines:
# suppresses the normal 'echo' of the command that is executed.
- means ignore the exit status of the command that is executed (normally, a non-zero exit status would stop that part of the build).
+ means 'execute this command under make -n' (or 'make -t' or 'make -q') when commands are not normally executed. See also the POSIX specification for make and also §9.3 of the GNU Make manual.
The + notation is a (POSIX-standardized) generalization of the de facto (non-standardized) mechanism whereby a command line containing ${MAKE} or $(MAKE) is executed under make -n.
(# is discussed in §5.2 of the GNU Make manual; - is described in §5.5; and §5.7.1 mentions the use of +.)
# prevents the command line from echoing out to the console. You can do it globally with -s or --keep-silent
- tells make to keep going, even if the command fails for some reason. You can do it globally via the -i flag (or --ignore-errors).
+ I was not familar with before you asked. As near as I can tell, it negates the effect of -n, -t, and -q, all of which basically tell make to not actually run the commands. So a line with a + at the front would get run anyway.
If you read the official Gnu Make manual, they are all mentioned in Chapter 5. In my old copy of the manual that was the chapter on "commands", but term du jour now seems to be "recipes".

Resources