3

I made a few changes to a bash script recently, and made a syntax error in a function. I know what the error is (spaces around the assignment: local max_loops = 4 instead of local max_loops=4).

However, I didn't spot this at first - it's a long script with a lot of output and it just output errors but continued running - the function returned true, so despite testing for success of the function I got no errors.

A simplified version is here (updated based on comments and answers below):

!/bin/bash
set -e

do_magic(){
    set -e
    local i=1
    local max_loops
    max_loops = 4   # This is the error i want the script/function to break on
    while [ $i -le $max_loops ]; do
        echo "Loop: $i"
        i=$(( i + 1 ))
    done
}

if do_magic; then
    echo "Ran OK"
else
    echo "Error running...."
fi

(My actual function does stuff in the loop and returns an error code if the stuff fails). This just outputs:

$ ./foo.sh
./foo.sh: line 1: !/bin/bash: No such file or directory
./foo.sh: line 8: max_loops: command not found
./foo.sh: line 9: [: 1: unary operator expected
Ran OK

My Question: Is there any generic way to say that functions with unhandled errors return a non-zero value? I tried set -e (I know I shouldn't need it twice, but tried both for safeties sake) and it doesn't seem to help.

I'd be happy either with the whole script breaking and stopping, or with the function breaking and returning a non-zero error. But i don't want to see "Ran OK" here...

I've fixed the actual error here, but I'd like to make the script more robust.

9
  • Aside: $[ ] is a non-POSIX-compliant extension for backward compatibility with ancient 1970s-era shells. Don't ever use it in new code -- i=$(( i + 1 )) is the POSIX-standardized syntax. Commented Dec 12, 2017 at 16:26
  • ...and [ isn't bash syntax, so it's not actually a shell syntax error. A failure with it is just the same as if some other random program you were calling were given incorrect command-line arguments, insofar as the shell is concerned. (Yes, it's implemented as a builtin, but they behave identically to external commands insofar as parsing, exit status, error handling, etc etc are concerned). Commented Dec 12, 2017 at 16:28
  • (I'd also strongly suggest do_magic() { with no function -- see wiki.bash-hackers.org/scripting/obsolete; function funcname() { is an amalgam between POSIX sh and ksh function declaration syntax, while being compatible with neither). Commented Dec 12, 2017 at 16:31
  • @CharlesDuffy It also took me a while to realize that, but the question is more: "Why does the script not fail on local max_loops = 4, even if set -e is in place?" Commented Dec 12, 2017 at 16:34
  • 1
    @hek2mgl, ...ahh -- yeah, this could have been much more clearly asked -- there's no reason for the function to have more than than one local line as its minimal reproducible example. Commented Dec 12, 2017 at 16:36

3 Answers 3

3

Basically syntax errors can't be handled by code because the syntax has to be parsed before the code can run. I think this is what you assume.


However, in this case it is not a syntax error in bash, it is a syntax error raised by the [ (or test) command builtin.

Note that [ is an external* command, it is more or less an alias to the test command. You can replace:

[ 1 -lt 2 ]

by:

test 1 -lt 2

for example.

This kind of error can only be detected at runtime of the bash script, and the bash script won't fail by default It is just like any other command that fails. Well, with one exception: If you use set -e, the [ command won't fail the script.

* nowadays realized as a bash builtin. but it is not bash syntax


How to solve it?

bash offers the [[ (extended comparison) for this (plus other advantages):

foo() {
    # Oops! missing the dash in front of 'lt'      
    if [[ 1 lt 2 ]] ; then
        echo "foo"
    fi  
}

echo "Don't print this"    
foo
echo "And this"

Since the [[ is bash syntax, not an external command, bash can handle this error at parsing stage, before the code runs. The result is that script doesn't run at all.

Sign up to request clarification or add additional context in comments.

7 Comments

it would, except that the script continues running after the syntax error! if it just bailed out, then I'd be fine with that... it's the fail-and-continue behaviour I don't like
Ah sorry, I got it wrong. This is not a bash syntax error, it is a test syntax error. Let me rephrase my answer...
Well, it's both local and test that complain... But yes, maybe not a strict bash syntax error
No, it is not a bash error at all. For bash the [ just looks like this [ SOME_STRING ]. SOME_STRING will be parsed by [ Do you see my point?
As I said, it's not just the test. The actual error is on the variable assignment above. I get two errors from the local max_loops line (line 6) and one from the test line (line 7) as in the output I show. If I change to [[ then the only difference is I don't get an error from the test line. i still get errors from the variable definition (line 6) but nothing from test - but the end result is the same: it treats the function call as successful even though errors were created and the loop didn't run
|
0

Use set -e.

Example (t.sh):

set -e
x = r
echo hello

Execution:

$ bash t.sh
t.sh: line 3: x: command not found
$ echo $?
127

EDIT: And you need to change the if do_magic as follows:

do_magic
if [ $? -eq 0 ]; then
    ...

8 Comments

Yeah, I wondered about that. Most advice is to avoid -e if you can, but I could try it. But I tried it and it doesn't work for functions like this. I added set -e directly after the #!/bin/bash in my example script. i still get the errors output then "Ran OK"...
The only reason I can imagine why your program does not stop is because you spawn subprocesses (with &), and these die, but the main program does not.
Look at my example program. no sub-processes or &
If set -e has been run, then the [ $? -eq 0 ] is pointless: If it isn't 0, then the if statement will never be reached.
@user803422, if do_magic "returns explicitly an error code", set -e will abort. See bash -c 'f() { return 1; }; set -e; f; echo "reached with value $?"' -- you never get to reached.
|
0

This is not a syntax error, but one of the most common Bash pitfalls

The local keyword is a Bash builtin. Be careful with local, because for historical reasons this statement returns a success status that you might not expect (Bash manual). Here is an example:

local x=$(false) # $? == 0

vs.

local x
x=$(false)       # $? == 1

But in your case, the expression

local max_loops = 4

is equivalent to

local max_loops
local = # error, $? == 1 - an invalid name is supplied
local 4 # error, $? == 1 

So, you have a command with the nonzero return status. Making script exit automatically with the -errexit shell flag doesn't help because of the execution context of do_magic, see here.

The following will work fine with set -e

do_magic
echo "Ran OK"

Just remember that if you rely on set -e, never change the context. Even executing your script as

if ./do_magic.sh; then
    :
fi

enough for set -e magic to disappear.

4 Comments

I have changed to having local max_loops on it's own line and max_loops = 4 on the line below that. I also set -e at the top of the script (before the function definition - and also even tried inside it). Nothing helps. I still get errors in the console and then Ran OK
@Adam, the following does not print "Success": set -e; local max_loops = 4; echo Success; but script reports error and exits
Yeah, but if the error is inside the function it seems to swallow it and still return true
Why not to verify that? set -e; foo() { local max_loops = 4; }; foo; echo Success;

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.