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 M
ath function with s
tring 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.
#!/bin/bash
so you can be sure you really are usingbash
rather than some other shell$
from both variable names inside the$(( ... ))
construct and try again$((12 - $)) ; echo foo
. Theecho
isn't executed. However, with((12 - $)) ; echo yeah
it works as expected.$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 inb
is interpreted as zero).