Arithmetic comparison in a shell - shell

i want to compare two number values in a shell script (sh) but it doesn`t work:
#!/bin/sh
let a=30
let b=100
let x=$a-$b
echo $a $b $x
[ $a < $b ] && { echo ok; }
That outputs:
30 100 -70
./x: line 6: 100: No such file or directory

I believe that should be -lt (which stands for less than) rather than "<". "<" is for string comparisons.
Edit: Actually looking at this now it seems clear what the problem is. The "<" character does file redirection so that's what the shell is trying to do. You can escape that character by doing \< instead but as originally stated that will do string comparison rather than numeric comparison.

Replace < with -lt
Also, lose the "let." This isn't Basic. Bill Gates and Steve Ballmer (Developers!) have nothing to do with this universe.

"let" is perfectly fine in shell, and has nothing to do with M$ basic or what.!
#OP , you can use bc to compare numbers, especially if you are also comparing floats. See here for similar example

#!/bin/sh
let a=30
let b=100
let x=$a-$b
echo $a $b $x
if(($a < $b)) then
echo ok
fi

Related

unable to use if statement properly

I am trying to use the simple conditional if statement as follows
val=-148.32
con=4.0
if [ $val > $con ]
then
echo 'akash'
else
echo 'mondal'
fi
in the above case I should get the output as mondal but I'm getting always akash. Can anyone plz let me know what mistake I'm doing?
Thank you.
The immediate problem is that a few of those backslashes are syntax errors.
The proper way to write this would be
val=-148.32
con=4.0
if [ $val -gt $con ]
then
echo 'akash'
else
echo 'mondal'
fi
though if you really wanted to, you could rewrite it as
val=-148.32;\
con=4.0;\
if [ $val -gt $con ];\
then\
echo 'akash';\
else\
echo 'mondal';\
fi
The ; separators are equivalent to newlines as statement separators (and the backslashes escape the actualy newline) -- as you can see, the separator is optional after then and else (but not before).
The > operator is supported by the [ (aka test) command, but it performs strict string comparison. The numeric comparison for greater-than is -gt.
The indentation is not syntactically important, but omitting it makes your code rather unreadable for human consumers.
However, as others have already remarked, Bash does not have any support for floating-point arithmetic. If you can rephrase your problem into an integer one, you should be able to use e.g.
val=-148.32
con=4.00
if [ ${val/.//} -gt ${con/.//} ]; then
echo 'akash'
else
echo 'mondal'
fi
but this requires the number of decimal points to be fixed, or at least predictable.
A common workaround is to use Awk instead.
val=-148.32
con=4.0
awk -v c="$con" '{ if ($1 > c) print "akash"; else print "mondal" }' <<<"$val"
I overcome the barrier in the following way
if awk "BEGIN {exit !($val > $con)}"
then
statement
else
statement
fi
where $val and $con are real variable

How can i fix the bad substitution?

I got loc_list0,1,2,3,
and i try to do it efficiently and type
b=0
while [ $b -lt 4 ]
do
grep -c "${loc_list$b[0]}" Record$b.txt
done
It says Bad Substitution on ${loc_list$b[0]}, but ok for Record$b. What is the reason behind? I am new to bash shell can anyone tell me how to fix it instead of writing duplicate codes.
Thanks man!
But another problems come when i want to use two varibales for iteration
thanks man, how about i got two variables
b and c which works as counting numbers of iteration
such that:
b=0
c=0
while [ $b -lt 5 ]
do
temp_length=( "${loc_list$b[#]}" )
while [ $c -lt ${#temp_length[#]} ]
do
...
c=$((c+1))
done
...
b=$((b+1))
done
how to fix the bad substitution this time?
You need to use indirect parameter substitution. With arrays, the index you want is considered part of the name.
name=loc_list$b[0]
grep -c "${!name}" Record$b.txt
Record$b.txt works because it is a simple string concatenation, Record + $b + .txt. You aren't try to further expand the result.

Two while loops behaving strangely, Bash script

I'm new to Bash scripting. I have written a script to help me get some info using ssh from bunch of servers. the IP address of first set of devices are from 101 to 148, and the other set are from 201 to 210.
#!/bin/bash
BASE=192.168.11
SD_START=101
SD_END=148
HD_START=201
HD_END=210
SD_counter=$SD_START
HD_counter=$HD_START
while [[ $SD_counter -le $SD_END ]]
do
ip=$BASE.$SD_counter
ssh $ip command1
SD_counter=$(($SD_counter +1))
if [ "$SD_counter"==148 ]
then
while [[ $HD_counter -le $HD_END ]]
do
ip=$BASE.$HD_counter
ssh $ip command2
HD_counter=$(($HD_counter +1))
done
fi
done > log_SD_HD
echo "Done!"
But for some reason command1 is executed on 192.168.11.101 first, then command2 is executed on ip range 192.168.11.201-192.168.11.210 which is the second while loop.
After that the first while loop continues till the end.
Why is this happening? I want the first while loop to be done before the second while loop. Could someone please point out what I'm doing wrong?
#0x1cf's answer provides the right pointer:
[ "$SD_counter"==148 ] doesn't work as expected.
Specifically: "$SD_counter"==148, based on bash's string synthesizing rules, is expanded to a single string literal: the value of $SD_counter is concatenated with literal ==148, and the resulting string literal is treated as a Boolean.
Since a non-empty string in a Boolean context always evaluates to true, [ "$SD_counter"==148 ] always evaluates to true due to lack of spaces around the ==.
Aside from that: in bash you should use [[ ... ]] rather than [ ... ] - it is more robust and provides more features.
Also note (as #0x1cf notes too) that - if using [ ... ] or [[ ... ]] - using the arithmetic operators is the right choice when dealing with numbers: -eq, -ne, -lt, -le, -gt, or -ge.
Generally, though, using (( ... )) expressions - arithmetic evaluation - provides more flexibility with numbers - see below.
That said, your code can be greatly simplified by using arithmetic evaluation - (( ... )) (see section ARITHMETIC EVALUATION in man bash):
It allows you to use C-style arithmetic and Boolean expressions.
If we combine this with bash's array variables, your code can be simplified to:
#!/usr/bin/env bash
BASE=192.168.11
START_INDICES=( 101 201 )
END_INDICES=( 148 210 )
COMMANDS=( command1 command2 )
numRanges=${#START_INDICES[#]}
for (( range = 0; range < numRanges; ++range )); do
cmd=${COMMANDS[range]}
for (( i=${START_INDICES[range]}; i<=${END_INDICES[range]}; ++i )); do
ip=$BASE.$i
ssh $ip $cmd
done
done > log_SD_HD
Note:
(( ... )) expressions DIFFER from normal bash assignments and conditionals in that you:
need NOT reference variables with $
need NOT double-quote variable references
you MAY have spaces around the assignment operator (=)
you MAY omit spaces around relational operators: (( SD_counter==148 )) DOES work.
( string1 ... ) creates an array with elements string1, ...; ${#arrayVar[#]} returns the count of elements of array variable arrayVar; ${arrayVar[ndx]} returns the element with (0-based) index ndx.
It's better to avoid ALL-UPPERCASE variable names such as BASE, as they may conflict with environment variables, which are by convention typically all-uppercase.
UPDATE
Hint: You can always use #!/bin/bash -x to trace and debug your scripts.
Maybe using two while loop is a good idea, just as V_Maenolis showed. However, to answer your question about what's wrong with your script, try this
Replace
if [ "$SD_counter"==148 ]
with
if [ "$SD_counter" -gt 148 ]
which works for me.
So there are two errors
There should be a space before and after == operator, that is to say, using A == B NOT A==B
The logic of comparing SD_counter == 148 is incorrect. Because when SD_counter hits 148, your script will run into the second while loop, and you'll get 147, 201, ..., 210, 148. Using -gt instead avoids the problem.
There is no reason to nest the loops from what you showed:
#!/bin/bash
BASE=192.168.11
SD_START=101
SD_END=148
HD_START=201
HD_END=210
SD_counter=$SD_START
HD_counter=$HD_START
while [[ $SD_counter -le $SD_END ]]
do
ip=$BASE.$SD_counter
ssh $ip command1
SD_counter=$(($SD_counter +1))
done> log_SD_HD
while [[ $HD_counter -le $HD_END ]]
do
ip=$BASE.$HD_counter
ssh $ip command2
HD_counter=$(($HD_counter +1))
done>> log_SD_HD
echo "Done!"

filtering files by number in bash error unary operator expected

I have a script that will find the distances between two atoms in pdb.
bash does not recognize decimals so I have put printf script to round the decimals.
and echo $b works fine and gives me a integer value.
but the if line for my filtering system does not work.
I get and error stating
[: -ge: unary operator expected
below is part of the script that I am working on.
a=$(awk '$2=='91'{x1=$6;y1=$7;z1=$8} $2=='180' {x2=$6;y2=$7;z2=$8} END{print sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) + (z1-z2)*(z1-z2))}' ${names}.pdb.$i)
b= printf %.0f $a
echo $b
if [ $b -ge 1 ] &&[ $b -le 9 ]; then
any help will be greatly appreciated. Thank you in advanced.
b= printf %.0f $a
This line sets the value of b to nothing for the duration of the printf command, which sends its output to stdout
echo $b
This prints a blank line.
You must not put whitespace around the = in an assignment, and to store the output of a command into a variable, you use this syntax:
b=$( printf %.0f $a )
You're getting the error because $b is empty, and this is what bash sees:
if [ -ge 1 ] &&[ -le 9 ]; then
-ge is expecting operands on both the left and the right, and it doesn't see one.
With bash, you should (almost) always prefer [[ ... ]] over [ ... ] -- the double bracket form is not fooled by variables containing empty strings.
You should always quote your "$variables" -- unless you know exactly when to not quote them.

Integer expression expected

I want to read my files line by line every 5 seconds. This time I just tried one-line bash command to do this.
And bash command is:
let X=1;while [ $X -lt 20 ];do cat XXX.file |head -$X|tail -1;X=$X+1;sleep 5;done
However I got the error like:
-bash: [: 1+1: integer expression expected
What's the problem?
btw, why can't we do $X < 20? (Instead we have to do -lt, less than?)
thx
Your assignment X=$X+1 doesn't perform arithmetic. If $X is 1, it sets it to the string "1+1". Change X=$X+1 to let X=X+1 or let X++.
As for the use of -lt rather than <, that's just part of the syntax of [ (i.e., the test command). It uses = and != for string equality and inequality -eq, -ne, -lt, -le, -gt, and -ge for numbers. As #Malvolio points out, the use of < would be inconvenient, since it's the input redirection operator.
(The test / [ command that's built into the bash shell does accept < and >, but not <= or >=, for strings. But the < or > character has to be quoted to avoid interpretation as an I/O redirection operator.)
Or consider using the equivalent (( expr )) construct rather than the let command. For example, let X++ can be written as ((X++)). At least bash, ksh, and zsh support this, though sh likely doesn't. I haven't checked the respective documentation, but I presume the shells' developers would want to make them compatible.
I would use
X=`expr $X + 1`
but that's just me. And you cannot say $X < 20 because < is the input-redirect operator.
The sum X=$X+1 should be X=$(expr $X + 1 ).
You can also use < for the comparison, but you have to write (("$X" < "20")) with the double parenthesis instead of [ $X -lt 20 ].

Resources