11

I was debugging one script, and discovered unexpected behavour of handling arithmetic syntax error.

Normally, when error happens, script is just continuing execution.

#!/bin/bash

func2 () {
  echo "Starting func2"
  jjjjj  # <- non-existing command. Error printed.
  echo "Finishing func2. This text can be shown normally."  # <- And function is executed further.
}

func2
echo "After func2"

with this script, I get the output:

$ ./test.sh
/home/user/test.sh: line 5: jjjjj: command not found
Finishing func2. This text can be shown normally.
After func2

But lets see such script:

#!/bin/bash

func1 () {
    echo "Starting func1"
    a=1
    b=""
    c=$(($a - $b))  # <- Here the problem happens (because $b is empty string).
    echo "Finishing func1. Why this text is not shown?"  # <- And unexpectedly, this line is never shown.
}

func1
echo "After func1"

I would expect to see the "Finishing func1" text, but surprisingly, it returns from function:

$ ./test.sh
Starting func1
/home/user/test.sh: line 7: 1 - : arithmetic syntax error: operand expected (error token is "- ")
After func1

The questions are:

  • Why this is behaving differently?
  • Is there a way to prevent such behavior of execution? I mean, not how to write the correct line instead of c=$(($a - $b)), but possibly about setting some variable, like set_never_interrupt_at_syntax_error=true.
  • Is it expected behavior, or is it a bash bug?

I tested this with bash version "5.3.3" gnu bash on Arch Linux.

15
  • 2
    Add a first line that is #!/bin/bash so you can be sure you really are using bash rather than some other shell Commented 2 days ago
  • 2
    Then remove the $ from both variable names inside the $(( ... )) construct and try again Commented 2 days ago
  • 1
    You can even get the same behavior with $((12 - $)) ; echo foo. The echo isn't executed. However, with ((12 - $)) ; echo yeah it works as expected. Commented 2 days ago
  • 2
    @Ashark, you have a syntax error (after expanding the $b), so the shell can't interpret the rest of the code, and it bails out. It seems reasonable to me. If you remove the $ from the variables, you no longer have a syntax error (an empty value in b is interpreted as zero). Commented 2 days ago
  • 1
    @Kusalananda it bails out – so what would you expect happens if you move that line from a function to the main script (before the last command)...? ;-) Commented 2 days ago

2 Answers 2

20

POSIX requires that expansion errors exit non-interactive shells (and produce an error message).

A syntax error upon arithmetic expansion is an expansion error

When in POSIX mode like when POSIXLY_CORRECT is in the environment or when called with -o posix or as sh, bash does exit.

When not in POSIX mode, it works like interactive invocations would: returns to the prompt¹, or where a prompt would be issued if the code in the script was entered interactively, so here after the function invocation.

Same happens for instance in:

$ bash -c 'echo "${a$}"; echo not reached'
bash: line 1: ${a$}: bad substitution

Except for the bash quirky behaviour when not in POSIX mode, all POSIX-like shells behave the same and exit upon arithmetic syntax error.

1 - is not a valid arithmetic expression, a - b is as per POSIX as long as a and b contain literal decimal, octal or hexadecimal representations of integer numbers, with behaviour unspecified if not.

As an extension, in Korn-like shells (ksh, zsh, bash), those variables may also contain any valid arithmetic expressions (including an empty one that is interpreted as 0) which are then evaluated recursively. You'll still get a fatal syntax error if those expressions are invalid or cause endless recursion:

$ a=1 b=1+ bash -c 'c=$(( a + b )); echo not reached'
bash: line 1: 1+: syntax error: operand expected (error token is "+")
$ a=b b=a bash -c 'c=$(( a + b )); echo not reached'
bash: line 1: b: expression recursion level exceeded (error token is "b")

In that particular instance of b being empty, in Korn-like shells,

c=$(( a - b ))

Would avoid the error and treat b as if it contained 0 (or any other arithmetic expression that yields 0).

POSIXly, you could do:

c=$(( (${a:-0}) - (${b:-0}) ))

Where we explicitly substitute 0 upon empty variables (also note the (...) around each in case they may be arithmetic expressions²).

In the general case, to be able to handle the error, you could have the expansion done as part of an eval command:

if ! command eval 'c=$(( a - b ))'; then
  echo Do something when invalid
fi

(command is needed in bash when in POSIX mode; without it, eval being a special builtin exits the shell upon error as POSIX requires for sh; POSIX command³ removes that special property).

That trick doesn't work in mksh or yash though which still exit upon syntax error in arithmetic expansion.

In zsh, mksh or bash, you can use:

(( c = a - b ))

In place of c=$(( a - b )) where syntax errors are not fatal. Note that it also returns a non-zero exit status if the expression yields 0 (making it difficult to detect the error condition) and would not change the value of $c upon syntax error.

Another approach could be to try the expansion first in a subshell:

expression='d += a - b'
if ( : "$(( $expression ))" ) 2> /dev/null; then
  c=$(( $expression ))
else
  echo Do something when invalid
fi

In zsh, you can handle the exception in an always block:

{
  c=$(( a - b ))
} always {
  there_was_an_error=$TRY_BLOCK_ERROR
  TRY_BLOCK_ERROR=0 # reset error condition
}
if (( there_was_an_error )); then
  echo do something upon syntax error
fi

For an arithmetic expansion where syntax errors don't raise exception but expand to some default value such as NaN / 0 / -1 / ERROR, since 5.3, bash supports mksh's function substitutions (also zsh since 5.10), where you could do:

math() {
  local IFS=' '
  command eval 'REPLY=$(( $* ))' || REPLY=NaN
}
c=${| math a - b ;}

(without the command for zsh). Note that bash arithmetic don't support floating points, so NaN there would be interpreted as the $NaN variable instead of the Not a Number special floating point value if used in another arithmetic expression.

In zsh, you could also use a Math function with string argument:

nonfatal() {
  eval ': $(( $1 ))' || (( NaN ))
}
functions -Ms nonfatal
c=$(( nonfatal(a - b) ))

In any case, using unsanitised data in arithmetic expressions is a very unwise thing to do. See Security Implications of using unsanitized data in Shell Arithmetic evaluation for details.


¹ Unless that code is in a subshell in which case it only exits the subshell.¹

² Though if those expressions reference other variables, you'd want to make sure those variables contain integer constants or you'd fall out of POSIX scope.

³ as opposed to zsh's command which predates POSIX' and is about running external commands (like yash's command -e) so wouldn't work for eval and anyway would not be needed in zsh.

6
  • But the shell does not exit. The function is not a subprocess. And the same outside a function does not abort the script at all. So maybe your explanation is the right one but the bash implementation is incorrect... Commented 2 days ago
  • 2
    @Raffa strace -f bash -c 'echo $((1+2))' just one process but strace -f bash -c 'echo $(echo 1+2)' is two. $() and $(()) do look similar but do very different things. So a subshell would not even make sense there. Commented 2 days ago
  • @HaukeLaging, see edit Commented 2 days ago
  • For clarity, you should add that the expansion being performed here results in c=$((1 - )) (sic!), which is a syntax error. This is unlike jjjjj which is syntacitcally correct. See answer of @ChrisDavies. Commented 2 days ago
  • @rexkogitans, I do already, see the paragraph that starts with 1 - is not..., and where I also make the point that jjjjj is only syntactically valid if jjjjj is a variable that contains a valid arithmetic expression. Commented 2 days ago
1

The problem is that you are using $a and $b inside the $(( … )) expression evaluator:

c=$(($a - $b))    # Wrong (almost always)

What's happening here is that $a and $b are evaluated as variables and their values are plugged into the expression. (This is a level of indirection that can be unexpected.) Since a=1 and b="" you get this, which generates the syntax error you're seeing:

c=$((1 - ))       # Arithmetic syntax error

The correct way to use variables inside $(( … )) is just as names (without the $ prefix):

c=$((a - b))      # Right
7
  • 2
    With a=1 b=1+ bash -c 'c=$(( a + b )); echo not reached', you still don't get the not reached though. That is a runtime syntax error in arithmetic expression is fatal whether a or $a is used. Commented 2 days ago
  • @StéphaneChazelas well, yes. "The value of a variable is evaluated as an arithmetic expression when it is referenced…" and 1+ isn't a valid arithmetic expression. Although why it halts program flow is something I can't explain (although I note your own answer does address this) Commented 2 days ago
  • 1
    also note what terdon mentioned in a comment, doing the same syntax error in an arithmetic statement (not expansion), doesn't trigger the same behaviour. E.g. here, the "hello" still gets printed: bash -c 'a=123 b=; ((c = a - $b)); echo hello' Commented 2 days ago
  • using a $var in an arithmetic expression can be useful if you need to choose an operator dynamically, e.g. op='*'; echo $(( 123 $op 4 )) (though I'd imagine most of the time it'd be a choice between plus and minus, and you could do that by multiplying with -1 * neg, with neg being zero or one). Commented 2 days ago
  • (oops, of course you need (1 - 2*neg) (or (-1 ** neg) but I don't think ** is standard). Or maybe even (1 - 2 * !!neg) to handle values other than zero or one sanely.) Commented 2 days ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.