How to use arithmetic in an if statement for Bash - bash

Hi Guys I am trying to perform a check in my bash script that needs to meet three conditions, I was able to make the first 2 conditions work in the if statement as I wanted, but when I wanted to implement a third check with some arithmetic using the && operator, the script does not even launch.
DOUBLE_CHECK=0
if [[ -z "$avail" && "$WIFI_ID" == "some_str" && 'expr $DOUBLE_CHECK % 2' -eq "0"]]; then
sudo caffeinate xterm -geometry 70x20+0+0 -fa monospace -fs 8 -e './script1.sh' & disown
fi
(($DOUBLE_CHECK++))
The Idea is I want the third check to have a number that increments over time inside my while loop and then checked, whenever it is divisible by 2 it passes the 3rd condition of the if statement

As noted in a separate answer, you could use backticks or preferably $() to expand the expression to the output of the inner command, like this :
if [[ -z "$avail" && $WIFI_ID == "some_str" && $(expr $DOUBLE_CHECK % 2) -eq "0" ]]
Please note I have added a (required) space before the final ]].
Another possibility is to use an arithmetic expression :
if [[ -z $avail && $WIFI_ID == "some_str" ]] && ((DOUBLE_CHECK % 2 == 0))
Please note I have removed harmless but unnecessary quotes : the double-bracketed conditional expression is not a normal command, it is special shell syntax, and it does not perform word splitting. I have left quotes around some_str, because equality/inequality comparisons will perform pattern matching on the right-hand expression if it is not quoted. There is also no $ before the variable name in the arithmetic expression : it works, but is not required inside (()) or $(()).
The expression could also be expressed as :
if [[ -z $avail && $WIFI_ID == "some_str" ]] && ! ((DOUBLE_CHECK % 2))
The reason for this is that (( )) has a return code of 0 if its numerical result is non-zero, and a non-zero return code otherwise.

There are a couple small issues
Your third condition should be evaluated using back-ticks ('`'), instead of quotes. Single-quotes in bash mean string literal. So, 'expr $DOUBLE_CHECK %2' -eq "0" will always be false
The check -eq is used to compare numbers. Your statement is using strings (to compare strings, use the normal == syntax)
Please try,
DOUBLE_CHECK=0
if [[ -z "$avail" ]] && [[ "$WIFI_ID" == "some_str" ]] && [[ `expr $DOUBLE_CHECK % 2` -eq 0 ]]; then
sudo caffeinate xterm -geometry 70x20+0+0 -fa monospace -fs 8 -e './script1.sh' & disown
fi
(($DOUBLE_CHECK++))

You can use [[ .. ]] for string logic (where = and == are equivalent) and (( ... )) for numeric comparison, and do away with expr:
if [[ -z "$avail" && "$WIFI_ID" = "some_str" ]] && (( DOUBLE_CHECK % 2 == 0 )); then
# your logic here
fi
Note that prefixing variables with $ is optional inside (( ... )) and quoting variables is optional inside [[ ... ]] since doesn't do word splitting and globbing there.

Related

Why are unset and empty variables numerically equal to zero with double brackets

The following code (tested with bash, zsh, and ksh, results may vary for other shells) returns a is 0 (num). The same happens for a=''. Results for assigning a=0 or a=1 are predictable. Quoting expressions does not change the result. So, why do double brackets treat null and empty variables as being numerically equal to zero?
unset a
#a=''
#a=0
#a=1
if [[ $a == 1 ]] ; then
echo 'a is 1 (string)'
fi
if [[ "$a" == 0 ]] ; then
echo 'a is 0 (string)'
fi
if [[ $a -eq 1 ]] ; then
echo 'a is 1 (num)'
fi
if [[ "$a" -eq 0 ]] ; then
echo 'a is 0 (num)'
fi
I'm deliberately avoiding the broader issues of single and double brackets, since it's very well covered other places on this site and elsewhere. Surprisingly, I've been unable to find anything that documents this particular behavior.
Further evidence:
unset a ; if [[ $a -gt -1 ]] ; then echo 'a > -1' ; fi
unset a ; if [[ $a -lt 1 ]] ; then echo 'a < 1' ; fi
I don't believe it is explicitly documented, but it's the -eq operator that forces a quasi-arithmetic context for its operands. Consider:
$ [[ "(3 + 5)" -eq 8 ]] && echo qed
qed
The behavior for variables is documented under ARITHMETIC EVALUATION:
A shell variable that is null or unset evaluates to
0 when referenced by name without using the parameter expansion syntax.
though it's not obvious that this should also apply to a string that results from a parameter expansion, and indeed the behavior is different in an arithmetic expression or arithmetic command itself:
$ unset a
$ (( a == 0 )) && echo zero
zero
$ (( $a == 0 )) && echo zero
bash: ((: == 0 : syntax error: operand expected (error token is "== 0 ")
Given that you already have $((...)) and ((...)) available for arithmetic, it's best to avoid -eq and the other arithmetic comparison operators inside [[; either use [ "$a" -eq 0 ] (which will raise an error if a is null or unset) or use (( a == 0 ).
It's well documented in the manual: 6.5 Shell Arithmetic
Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax. A shell variable that is null or unset evaluates to 0 when referenced by name without using the parameter expansion syntax.
and
A null value evaluates to 0.
Your tests are running into the 2nd case.
I can't tell you the design rationale behind it. It is handy in practice though:
unset a
(( a++ ))
echo $a # => 1
Also, other languages do the same thing:
$ awk 'BEGIN {if (unset_variable == 0) print "zero"}'
zero
$ perl -E 'if ($unset_variable == 0) {say "zero"}'
zero
So, why do double brackets treat null and empty variables as being numerically equal to zero?
From bash shell manual 6.5 Shell Arithmetic:
A shell variable that is null or unset evaluates to 0 when referenced by name without using the parameter expansion syntax
The quotes inside bash extension [[ compound command do not matter, because word splitting expansion is not performed inside [[.

Multiple conditions on bash 'if' statement

i'm having trouble with if in bash.
basically i'm trying to do this in bash:
if ((x='r')&&(y='s'))||((x='s')&&(y='p')) then
echo "bluh"
but i've just can't find the proper way, used square brackets, 2 square brackets, round brackets.
but it just won't work...
(( )) are for bash arithmetic, instead you need [[ ]] bash test :
if [[ ( $x == r && $y == s ) || ( $x == s && $y == p ) ]]; then
echo "bluh"
fi
[[ is a bash keyword similar to (but more powerful than) the [ command. See http://mywiki.wooledge.org/BashFAQ/031 and http://mywiki.wooledge.org/BashGuide/TestsAndConditionals. Unless you're writing for POSIX sh, we recommend [[.

Why does this if statement get evaluated to true in bash?

# prints "here" when run in bash
if [[ ((9 > 220)) ]]; then echo "here"; fi
I'm confused why the above if statement gets evaluated to true. Wouldn't ((9 > 220)) evaluate to false which would make the if statement false?
The code below behaves as expected. I'm confused why using the double parentheses in a double brackets "if" isn't working above though.
# doesn't print anything
if ((9 > 220)); then echo "here"; fi
There's a fundamental difference between those two compound commands inside a conditional construct.
[[ expression ]] compound command
In your first example you're using the [[ expression ]] command:
if [[ ((9 > 220)) ]]; then echo "here"; fi
where parenthesis are treated merely as grouping operators, used to override the normal precedence of other operators (like !, &&, ||, >, -gt, -e, etc.). The > operator in this case is a lexicographic greater than.
This is nicely described in man bash:
[[ expression ]]
Return a status of 0 or 1 depending on the evaluation of the conditional expression expression. Expressions are composed of the primaries described below under CONDITIONAL EXPRESSIONS. Word splitting and pathname expansion
are not performed on the words between the [[ and ]]; tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution, and quote removal are performed. Conditional operators such as -f must be unquoted to be recognized as primaries.
When used with [[, the < and > operators sort lexicographically using the current locale.
So, to compare integers in the [[ compound command, you can use the conditional expression operators, the same one used by test and [ commands. For example like this:
if [[ 9 -gt 220 ]]; then echo "here"; fi
The result is the same like when the -gt operator is grouped with parenthesis:
if [[ ((9 -gt 220)) ]]; then echo "here"; fi
Alternatively, you can use the arithmetic expansion and exploit the fact that the boolean results are represented as "0" or "1":
if [[ $((9 > 200)) == 1 ]]; then echo "here"; fi
(( expression )) compound command
In your second example, you're using the (( expression )) command:
if ((9 > 220)); then echo "here"; fi
where the expression is evaluated according to the rules of the shell arithmetic. The man bash says:
((expression))
The expression is evaluated according to the rules described below under ARITHMETIC EVALUATION. If the value of the expression is non-zero, the return status is 0; otherwise the return status is 1. This is exactly equivalent
to let "expression".
It is because using double brackets makes it a lexicographical comparison. That means that it checks for which sequence is longer, like sorting alphabetically. More information about lexicographical comparisons here
Inside of [[ ]], the parentheses don't trigger an arithmetic context, they're interpreted purely for grouping/precedence1. All of these are equivalent:
$ [[ 0 < 1 ]] && echo yes
yes
$ [[ (0 < 1) ]] && echo yes
yes
$ [[ ((0 < 1)) ]] && echo yes
yes
$ [[ (((0 < 1))) ]] && echo yes
yes
$ [[ ((((0 < 1)))) ]] && echo yes
yes
If we have unbalanced parentheses, Bash complains about that:
$ [[ ((((0 < 1))) ]] && echo yes
bash: unexpected token `]]', expected `)'
bash: syntax error near `]]'
This all being said, < and > within [[ ]] are for lexicographical string comparison, so the statement above just checks if 0 is lexicographically sorted before 1 (it is).
Observe:
$ [[ 11 < 2 ]] && echo yes
yes
To compare numbers, you have to use -gt, -lt, -ge, -le, eq, ne instead of >, <, >=, <=, =, !=:
$ [[ 11 -lt 2 ]] && echo yes || echo no
no
Since you're already using (( )), you could use just that for comparing numbers:
$ (( 11 < 2 )) && echo yes || echo no
no
This is the simplest and clearest method in my opinion, if you know you have Bash at your disposal.
1 See the manual about [[ ]]:
Expressions may be combined using the following operators, listed in
decreasing order of precedence:
( expression )
Returns the value of expression. This may be used to override the normal precedence of operators.
Hat tip to glenn jackman for pointing to the manual in his comment to another answer.
The double parentheses is an arithmetic context. If you use the brackets, you have to use -lt for less than and -gt for greater than. See Comparing numbers in Bash.

Condition evaluating unexpectedly

i am trying to write a simple bash script for a custom screensaver, i want simple to go the screen black if in idle, and go back to normal if not.
#!/bin/bash
#sets display gamma very low, for screensaver purposes
idle=false
idle_after=3000 #in milliseconds
while true; do
#if system is idle
if [[ idle_now=$(xprintidle) -gt "$idle_after" && "$idle"=false ]] ;then
echo "1"
`xrandr --output HDMI-0 --brightness 0.01`
idle=true
fi
if [[ idle_now=$(xprintidle) -lt "$idle_after" && "$idle"=true ]] ; then
echo "2"
`xrandr --output HDMI-0 --brightness 1` #set screen back to normal
exit
fi
done
i really don't know why the second if query gets executed first.
i thought the idle variable is initialized "false" at startup
can someone explain this to me ? and if someone has improvements to give on my approach i 'would very appreciate that , thanks
Your test expressions are wrong.
The reason for 2nd if was executed first is that there are no spaces between operands and operators inside test ([[ and ]]).
For better understanding, let's see a possible variable substitution in the first loop assuming that xprintidle returns 10:
[[ idle_now=$(xprintidle) -lt "$idle_after" && "$idle"=true ]]
# becomes
[[ idle_now=10 -lt 3000 && false=true ]]
Now you wanted compare 10 against 3000, but now you're comparing the string "idle_now=10" against 3000. As any string is converted to zero if the characters are not digits, its like you did:
[[ 0 -lt 3000 && false=true ]]
# that becomes
[[ <true> && false=true ]]
Now, the second operand is also one string "false=true" (not a comparison), and any string converted to boolean is false when it's empty. That's not your case, the string has 10 characters, so it's evaluated to true.
[[ <true> && <true> ]]
# that becomes
<true>
Note: I used <true> and <false> just to clarify, they really are internal representation of boolean values.
Fixing it
I imagine idle_now was meant to be a variable that it's never used, so we'll ignore it.
So the if expressions should be:
[[ "$(xprintidle)" -gt "$idle_after" && "$idle" = false ]]
# and
[[ "$(xprintidle)" -lt "$idle_after" && "$idle" = true ]]

Why does adding spaces around bash comparison operator change the result?

Could someone explain why spaces around == change the comparison result? The following:
if [[ 1 == 2 ]] ; then echo ok ; fi
prints nothing, while
if [[ 1==2 ]] ; then echo ok ; fi
prints ok
"1==2" is a single 4-character string, not an expression involving the == operator. Non-empty strings always evaluate to true in the context of the conditional expression [[ ... ]]. Whitespace is mandatory around the == operator.
Like everything else in bash, the contents of [[ ... ]] are simply a white-space-separated list of arguments. The bash grammar doesn't know how to parse conditional expressions, but it does know how to interpret a list of 3 arguments like 1, ==, and 2 in the context of the [[ ... ]] compound command.
Because it's just a string, consider testing :
[[ foobar ]]
it will be true.
This is useful to test if a variable is set or not like in this example :
x='foobar'
[[ $x ]] # true
and now
x=''
[[ $x ]] # false
Finally
The spaces are mandatory in a test expression

Resources