Problem with floating point comparison - bash

I am trying to check if a value I read from a text file is zero:
[[ $(echo $line | cut -d" " -f5) -gt 0 ]] && [[ $(echo $line | cut -d" " -f7 | bc -l) -eq 0 ]]
With the first condition there is no problem because f5 are integers. The problem comes form the second condition. I receive this error message:
[[: 1.235: syntax error: invalid arithmetic operator (error token is ".235")
I have tried several suggestions I found in different forums such as using echo $line | cut -d" " -f7 | bc -l with and without double quotes, etc. However, the error persist. f7 is a positive number and is given with 3 decimal places. Removing decimals or approximating is not an option because I need the result to be exactly zero (0.000).

Generally, you can't compare floating-point numbers for equality. This is because the binary representation of decimal numbers is not precise and you get rounding errors. This is the standard answer that most others will give you.
In this specific case, you don't actually need to compare floating-point numbers, because you're just testing whether some text represents a specific number. Since you're in shell, you can either use a regular string compare against "0.000" - assuming your data is rounded in that way - or using regular expressions with grep/egrep. Something like
egrep -q '0(|\.0+)'
Will match 0, 0.0, 0.00, etc, and will exit indicating success or failure, which you can use in the surrounding if statement:
if cut and pipe soup | egrep ... ; then
...
fi

Use a string comparison instead. Replace:
-eq 0
with:
= '0.000'
TZ:
Script section from comment:
for clus in $(ls *.cluster) ; do
while read line ; do
if [[ $(echo $line | cut -d" " -f11) -gt 0 ]] && [[ "$(echo $line | cut -d" " -f15 | bc -l)" = '0.000' ]] ; then
cat $(echo $line | cut -d" " -f6).pdb >> test/$(echo $line | cut -d" " -f2)_pisa.pdb
fi
done < $clus
done
My pseudo-Python interpretation:
for clus in *.cluster:
for line in clus:
fields = line.split(' ')
# field numbers are counting from 1 as in cut
if int(field 11) > 0 and str(field 15) == '0.000':
fin_name = (field 6) + '.pdb'
fout_name = (field 2) + '_pisa.pdb'
cat fin_name >> fout_name
Is that what you intended?

Related

Compare floating point numbers using regex in Bash

I need to check if current NTP offset is biger than 2.xxxxx , where xxx is any number
for example 2.005596757,2.006086349
offset=$(ntpdate -q 1.2.3.4 | head -1 | cut -d " " -f 6 | sed "s/.$//")
echo $offset
current offset variable is: 0.841816 so need to compare if X.XXXXXX is bigger or equal to 2.XXXXXXXXX , where X is any number in range [0-9]
offset=$(ntpdate -q 10.160.82.10 | head -1 | cut -d " " -f 6 | sed "s/.$//")
if [ $offset -ge ([2]+\.?[0-9]*) ]
then
echo "offset too high"
fi
But getting error
./1.sh: line 9: syntax error near unexpected token `('
./1.sh: line 9: `if [ $offset -ge ([2]+\.?[0-9]*)|([0-9]*\.[0-9]+) ]'
Why don't you compare only the integer part? Like:
if [ ${offset%.*} -ge 2 ]; then
echo offset too high
fi
For comparing a floating point number it is better to use awk or perl as bash can only handle integer numbers.
You may consider this awk solution that eliminates head, cut and sed as a bonus:
if ntpdate -q 1.2.3.4 | awk 'NR == 1 && $6 < 2 {exit 1} {exit 0}'; then
echo 'offset too high'
fi

How to cut variables which are beteween quotes from a string

I had problem with cut variables from string in " quotes. I have some scripts to write for my sys classes, I had a problem with a script in which I had to read input from the user in the form of (a="var1", b="var2")
I tried the code below
#!/bin/bash
read input
a=$($input | cut -d '"' -f3)
echo $a
it returns me a error "not found a command" on line 3 I tried to double brackets like
a=$(($input | cut -d '"' -f3)
but it's still wrong.
In a comment the OP gave a working answer (should post it as an answer):
#!/bin/bash
read input
a=$(echo $input | cut -d '"' -f2)
b=$(echo $input | cut -d '"' -f4)
echo sum: $(( a + b))
echo difference: $(( a - b))
This will work for user input that is exactly like a="8", b="5".
Never trust input.
You might want to add the check
if [[ ${input} =~ ^[a-z]+=\"[0-9]+\",\ [a-z]+=\"[0-9]+\"$ ]]; then
echo "Use your code"
else
echo "Incorrect input"
fi
And when you add a check, you might want to execute the input (after replacing the comma with a semicolon).
input='testa="8", testb="5"'
if [[ ${input} =~ ^[a-z]+=\"[0-9]+\",\ [a-z]+=\"[0-9]+\"$ ]];
then
eval $(tr "," ";" <<< ${input})
set | grep -E "^test[ab]="
else
echo no
fi
EDIT:
#PesaThe commented correctly about BASH_REMATCH:
When you use bash and a test on the input you can use
if [[ ${input} =~ ^[a-z]+=\"([0-9]+)\",\ [a-z]+=\"([0-9])+\"$ ]];
then
a="${BASH_REMATCH[1]}"
b="${BASH_REMATCH[2]}"
fi
To extract the digit 1 from a string "var1" you would use a Bash substring replacement most likely:
$ s="var1"
$ echo "${s//[^0-9]/}"
1
Or,
$ a="${s//[^0-9]/}"
$ echo "$a"
1
This works by replacing any non digits in a string with nothing. Which works in your example with a single number field in the string but may not be what you need if you have multiple number fields:
$ s2="1 and a 2 and 3"
$ echo "${s2//[^0-9]/}"
123
In this case, you would use sed or grep awk or a Bash regex to capture the individual number fields and keep them distinct:
$ echo "$s2" | grep -o -E '[[:digit:]]+'
1
2
3

Convert Floating Point Number To Integer Via File Read

I'm trying to get this to work when the "line" is in the format ###.###
Example line of data:
Query_time: 188.882
Current script:
#!/bin/bash
while read line; do
if [ $(echo "$line" | cut -d: -f2) -gt 180 ];
then
echo "Over 180"
else
echo "Under 180"
fi
done < test_file
Errors I get:
./calculate: line 4: [: 180.39934: integer expression expected
If you have:
line='Query_time: 188.882'
This expression:
$(echo "$line" | cut -d: -f2) -gt 180
Will give an error invalid arithmetic operator since BASH cannot handle floating point numbers.
You can use this awk command:
awk -F ':[ \t]*' '{print ($2 > 180 ? "above" : "under")}' <<< "$line"
above
You can use this awk:
$ echo Query_time: 188.882 | awk '{ print ($2>180?"Over ":"Under ") 180 }'
Over 180
It takes the second space delimited field ($2) and using conditional operator outputs if it was over or under (less than or equal to) 180.

Bash - invalid arithmetic operator

I'm trying to study for a test and one of the subjects are bash scripts.
I have the following txt file :
123456 100
654321 50
203374111 86
I need to get the averages of the scores (the numbers in the second column).
This is what I have written :
cat $course_name$end | while read line; do
sum=`echo $line | cut -f2 -d" "`
let total+=$sum
done
I have tried with
while read -a line
and then
let sum+=${line[1]}
But I'm still getting the same error mentioned in the header.
I love AWK:
awk -F\* '{sum+=$3} END {print sum/NR}' x.txt
So in x.txt are values are stored. Please note that many answers don't actually compute the average, as they need to divide by the line numbers in the end. Often it will be performed by a wc -l < x.txt but in my solution you will get it almost for free.
cat your_file_name.txt | cut -f2 -d" " | paste -sd+ | bc
This should do the job!
You are very close, this works for me:
while read line; do
sum=$(echo $line | cut -f2 -d" ")
echo "sum is $sum"
let total+=$sum
echo "total is $total"
done < file
echo "total is $total"
As you can see, there is no need to use cat $course_name$end, it is enough to do
while read line
do
done < file
Also, it is more recommendable to use
sum=$(echo $line | cut -f2 -d" ")
rather than
sum=`echo $line | cut -f2 -d" "`
Or even
sum=$(cut -f2 -d" " <<< "$line")
There's no need to use cat as well as read; you can redirect the contents of the file into the loop. You also don't need to use let for arithmetic.
sum = 0
count = 0
while read id score; do
(( sum += score )) && (( ++count ))
done < "$course_name$end"
echo $(( sum / count ))
This will give you an integer result, as bash doesn't do floating point arithmetic. To get a floating point result, you could use bc:
bc <<< "scale=2;$a/$b"
This will give you a result correct to 2 decimal places.

how do i verify presence of special characters in a bash password generator

Supposed to be a simple bash script, but turned into a monster. This is the 5th try. You don't even want to see the 30 line monstrosity that was attempt #4.. :)
Here's what I want to do: Script generates a random password, with $1=password length, and $2=amount of special characters present in the output.
Or at least, verify before sending to standard out, that at least 1 special character exists. I would prefer the former, but settle for the latter.
Here's my very simple 5th version of this script. It has no verification, or $2:
#!/bin/bash
cat /dev/urandom | tr -dc [=!=][=#=][=#=][=$=][=%=][=^=][:alnum:] | head -c $1
This works just fine, and it's a sufficiently secure password with Usage:
$ passgen 12
2ZuQacN9M#6!
But it, of course, doesn't always print special characters, and it's become an obsession for me now to be able to allow selection of how many special characters are present in the output. It's not as easy as I thought.
Make sense?
By the way, I don't mind a complete rework of the code, I'd be very interested to see some creative solutions!
(By the way: I've tried to pipe it into egrep/grep in various ways, to no avail, but I have a feeling that is a possible solution...)
Thanks
Kevin
How about this:
HASRANDOM=0
while [ $HASRANDOM -eq 0 ]; do
PASS=`cat /dev/urandom | tr -dc [=!=][=#=][=#=][=$=][=%=][=^=][:alnum:] | head -c $1`
if [[ "$PASS" =~ "[~\!#\#\$%^&\*\(\)\-\+\{\}\\\/=]{$2,}" ]]; then
HASRANDOM=1
fi
done
echo $PASS
Supports specifying characters in the output. You could add characters in the regex though I couldn't seem to get square brackets to work even when escaping them.
You probably would want to add some kind of check to make sure it doesn't loop infinitely (though it never went that far for me but I didn't ask for too many special characters either)
Checking for special characters is easy:
echo "$pass" | grep -q '[^a-zA-Z0-9]'
Like this:
while [ 1 ]; do
pass=`cat /dev/urandom | tr -dc [=!=][=#=][=#=][=$=][=%=][=^=][:alnum:] | head -c $1`
if echo "$pass" | grep -q '[^a-zA-Z0-9]'; then
break;
fi
done
And finally:
normal=$(($1 - $2))
(
for ((i=1; i <= $normal; i++)); do
cat /dev/urandom | tr -dc [:alnum:] | head -c 1
echo
done
for ((i=1; i <= $2; i++)); do
cat /dev/urandom | tr -dc [=!=][=#=][=#=][=$=][=%=][=^=] | head -c 1
echo
done
) | shuf | sed -e :a -e '$!N;s/\n//;ta'
Keep it simple... Solution in awk that return the number of "special characters" in input
BEGIN {
FS=""
split("!##$%^",special,"")
}
{
split($0,array,"")
}
END {
for (i in array) {
for (s in special) {
if (special[s] == array[i])
tot=tot+1
}
}
print tot
}
Example output for a2ZuQacN9M#6! is
2
Similar approach in bash:
#!/bin/bash
MyString=a2ZuQacN9M#6!
special=!##$%^
i=0
while (( i++ < ${#MyString} ))
do
char=$(expr substr "$MyString" $i 1)
n=0
while (( n++ < ${#special} ))
do
s=$(expr substr "$special" $n 1)
if [[ $s == $char ]]
then
echo $s
fi
done
done
You may also use a character class in parameter expansion to delete all special chars in a string and then apply some simple Bash string length math to check if there was a minimum (or exact) number of special chars in the password.
# example: delete all punctuation characters in string
str='a!#%3"'
echo "${str//[[:punct:]]/}"
# ... taking Cfreak's approach we could write ...
(
set -- 12 3
strlen1=$1
strlen2=0
nchars=$2
special_chars='[=!=][=#=][=#=][=$=][=%=][=^=]'
HASRANDOM=0
while [ $HASRANDOM -eq 0 ]; do
PASS=`cat /dev/urandom | LC_ALL=C tr -dc "${special_chars}[:alnum:]" | head -c $1`
PASS2="${PASS//[${special_chars}]/}"
strlen2=${#PASS2}
#if [[ $((strlen1 - strlen2)) -eq $nchars ]]; then # set exact number of special chars
if [[ $((strlen1 - strlen2)) -ge $nchars ]]; then # set minimum number of special chars
echo "$PASS"
HASRANDOM=1
fi
done
)
You can count the number of special chars using something like:
number of characters - number of non special characters
Try this:
$ # define a string
$ string='abc!d$'
$ # extract non special chars to letters
$ letters=$(echo $string | tr -dc [:alnum:] )
$ # substract the number on non special chars from total
$ echo $(( ${#string} - ${#letters} ))
2
The last part $(( ... )) evaluate a mathematical expression.

Resources