Strange comparison results - bash

I have some bash issues:
This is expected:
[[ 0 -eq 0 ]] && echo "equal!"
> equal!
This is not:
[[ "" -eq 0 ]] && echo "equal!"
> equal!
Why is "" equal to 0?
How can I check for numeric equality?

This is because Bash tries hard to convert whatever you put into both sides of -eq into integers, and will convert the empty string to zero. The conversions are far from trivial. Here's how I expect the code parses numbers, without actually having read it:
$ [[ x -eq 0 ]] && echo "equal!"
equal!
After Bash detects a numeric context (-eq) it starts creating a number from zero on the left side, scans and finds x, discards it, scans and finds whitespace, and therefore considers the left side zero. Hence the above is equivalent to [[ 0 -eq 0 ]]
$ [[ 0x10 -eq 16 ]] && echo "equal!"
equal!
Starting from zero again, Bash sees a zero (before the "x") and goes into "alternate base" mode, finds an "x" and goes into hexadecimal mode, and reads the remaining digits ("10") as a hexadecimal number.
$ [[ 00x10 -eq 16 ]] && echo "equal!"
bash: [[: 00x10: value too great for base (error token is "00x10")
After going into "alternate base" mode after seeing a zero Bash sees a number (the second zero), and therefore goes into octal mode. x is considered a "numeric character" in this mode because it can be used in higher bases, but "x" is not a valid octal digit, so it fails.
See the Bash manual for more.

Related

Shell Scripting - Numeric Checks and if statement questions

I'm relatively new here and to the coding world. I'm currently taking a class in Shell Scripting and I'm a bit stuck.
I'm trying to do a little extra credit and get the script to check for command line arguments and if none or only 1 is given, prompt the user to input the missing values.
For the most part I've been able to get most of it to work except for when it comes to the numeric check part. I'm not completely sure that I am doing the nested if statements correctly because it's displaying both the "if" echo and the "else" echo.
My script so far:
q=y
# Begins loop
until [[ $q == n ]];do
# Checks command line arguments
if [[ $# -lt 2 ]];then
# Asks for second number if only 1 argument.
if [[ $# == 1 ]];then
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
echo "You've chosen $1 as your first number and $y as your second number."
break
else
echo "This is not a valid value, please try again."
fi
# Asks for both numbers if no arguments.
else
read -r -p "Please enter your first number: " x
if [[ x =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
echo "You've chosen $x as your first number and $y as your second number."
fi
# If both command line arguments are provided, echo's arguments, and sets arguments as x and y values.
else
echo "You've chosen $1 as your first number and $2 as your second number."
x=$1
y=$2
fi
read -r -p "Would you like to try again? (n to exit): " q
done
When I run it I get this for output:
Please enter your first number: 1
This is not a valid value, please try again.
Please enter your second number: 2
This is not a valid value, please try again.
You've chosen 1 as your first number and 2 as your second number.
Please enter your first number:
And will just continue to loop without breaking. Any help/guidance would be greatly appreciated, thank you.
In your expression:
if [[ x =~ [1-9] ]]; then
You are actually comparing the string literal "x" with the regex. What you want is the variable:
if [[ $x =~ [1-9] ]]; then
This will interpolate the variable first in order to compare the variable's value with the regex. I think this change also applies to some of the other comparison expressions in your code.
However, as glenn jackman and user1934428 have commented, this will also match things like foo1bar, which is probably not what you want. To fix this, you can add start/end matchers to your regex. Finally, you may want to match even if the input has leading or trailing spaces. One way to do this is to add some [[:space:]]*'s to match zero or more spaces around your [1-9]:
if [[ $x =~ ^[[:space:]]*[1-9][[:space:]]*$ ]]; then
So, to break down the regex:
^ start of input
[[:space:]]* zero or more whitespaces
[1-9] a single digit, 1-9
[[:space:]]* zero or more whitespaces
$ end of the input
I'm assuming from your question than you only want to match on a single digit, not, for example, 12, or the digit 0. To match those would require a couple more regex tweaks.
and...glob pattern
Just because glen jackman's answer led me down a bash man page adventure 🏄 and I wanted to try them out, this is a glob pattern version (note the == instead of =~):
if [[ $x == *([[:space:]])[1-9]*([[:space:]]) ]]; then
It's basically the same pattern. But notably, glob patterns seem to be implicitly anchored to the start/end of the string being matched (they are tested against the entire string) so they don't need the ^ or $, while regular expressions match against substrings by default, so they do need those additions to avoid foo1bar matching. Anyway, probably more than you cared to know.
Here's an alternate implementation, for your consideration: hit me up with any questions
#!/usr/bin/env bash
get_number() {
local n
while true; do
read -rp "Enter a number between 1 and 9: " n
if [[ $n == [1-9] ]]; then
echo "$n"
return
fi
done
}
case $# in
0) first=$(get_number)
second=$(get_number)
;;
1) first=$1
second=$(get_number)
;;
*) first=$1
second=$2
;;
esac
# or, more compact but harder to grok
[[ -z ${first:=$1} ]] && first=$(get_number)
[[ -z ${second:=$2} ]] && second=$(get_number)
echo "You've chosen $first as your first number and $second as your second number."
This uses:
a function to get a a number from the user, so you don't have so much duplicated code,
a case statement to switch over the $# variable
input validation with the == operator within [[...]] -- this operator is a pattern matching operator, not string equality (unless the right-hand operand is quoted)
Note that [[ $x =~ [1-9] ]] means: "$x contains a character in the range 1 to 9" -- it does not mean that the variable is a single digit. If x=foo1bar, then the regex test passes.

why does float comparison works without bc in a AND_IF or "&&" command

The only way I could find out to compare floats in shell is:
A=12.3
B=12.2
if [ $(bc <<< "$B <= $A") -eq 1 ]
Direct comparison, as far as I know, doesn't happen.
but strangely the following code compares floats without bc:
A=13.7
B=13.2
[[ $A > $B ]] && echo "A is greater than B"
[[ $A < $B ]] && echo "A is less than B"
This returns:
A is greater than B
As far as I know && executes the second command only and only if the first command returns with an exit status zero.
However, as suggested by Pixel Chemist in the comments if we attempt to use negative numbers in the second methods, it doesn't work and gives the opposite results.
Can someone please explain how is the second method working without bc.

integer expected error...blank line?

I'm getting a "integer expression expected error" when doing the following:
NM=$(<file)
if test $NM -gt 0
then
echo "workflow 1 would follow here"
else
if test $NM -lt 0
then
echo "workflow 2 would go after this"
else
echo "something else"
fi
fi
The file from which I am getting $NM assigned only contains one number (always an integer). This file is the output of a gmtmath operation.
I've noticed that if I open that file it has an extra line below the line containing the number and, if I manually delete that empty line my loop works and I don't get the error. However, I've tried to use sed in various ways to automatically delete empty lines and it deletes the entire content of the file, including the number. Any ideas??
After reading some comments, I'm wondering if you are getting some unprintable characters. I've tested this by putting in a \r before the \n, was finally able to reproduce. Here's a cleaned up version of your code, using tr to remove some extra characters.
NM=$(tr -cd '[:graph:]' <file)
if [[ $NM -gt 0 ]]; then
echo "workflow 1 would follow here"
elif [[ $NM -lt 0 ]]; then
echo "workflow 2 would go after this"
else
echo "something else"
fi
I could have used other classes other than [:graph:]. I just end up using [:graph:] often, but to each their own.
You want to test ${#NM} is greater than zero (e.g. the number of characters in $NM). Currently you are testing the contents of the string against 0 and bash is telling you it needs a number. The same applies for each of your tests where you need a number.
If you want to test whether the word "$NM" is greater than 0 or less than 0 use if ((NM > 0)); then ... or if ((NM < 0)); then ....
You can also use the older test expression if [ "$NM" -gt 0 ]; then... or if test "$NM" -gt 0 ; then... just be aware you will generate your error if "$NM" cannot be interpreted as a number. (you can redirect stderr to avoid that, but that isn't wise here).

Double-bracket if-statement not working

if [[ $nb_dd > 0 ]]; then
INPUT_TYPE="txt"
fi
if [ $nb_dd > 0 ]; then
INPUT_TYPE="txt"
fi
The first check (using double brackets) fails to execute, causing the script to exit upon error as $INPUT_TYPE is not populated - but the latter version (single bracket) works, correctly setting $INPUT_TYPE. Why is this?
I'm running on OS X (10.11.4) (echo $SHELL = /bin/bash) - this is from a widely distributed suite of tools which were presumingly built on a Linux platform - and the .sh script otherwise works for the author so it may be a platform-specific issue although I can't think why.
They both "work" for me, however you are using the wrong type of test. The > inside [[ is doing a textual comparison. For an arithmetic comparison either use the old -gt or (better) the correct brackets - double parentheses:
if (( nb_dd > 0 )); then
INPUT_TYPE="txt"
fi
Note that the $ is not used, since only numerics can be compared inside ((...)) (using a $ inside might work but can give strange side-effects because of expansion order).
You must gt and lt to make comparisons,
if [[ $nb_dd -gt 0 ]];
then INPUT_TYPE="txt";
fi
if [ $nb_dd -gt 0 ];
then INPUT_TYPE="txt";
fi
Double brackets just extend posix functionality
> means nothing in posix compliant test expressions, and it just calls the output redirection.
[ 2 > 3 ] && echo "shouldn't print this"
[ 2 -gt 3 ] && echo "this isn't printed"
On extensions expressions to the posix ones it means sting comparison.
[[ "aa" > "ab" ]] && echo "doesn't print"
[[ "aa" > "aa" ]] && echo "doesn't print"
[[ "ab" > "aa" ]] && echo "prints"

bash, prompt for numerical input

d is an internal server lookup tool I use.
I am looking to allow a user to input any number between 0 (or 1) and 9999 (let's call this userinput) and have it display the result of:
d $userinput (e.g. 1234)
Then manipulate the results of that lookup (below gets rid of everything but the IP address to ping later):
grep -E -o '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'`
I know I need to use the while true; do read $blah etc etc. I am just not familiar with read enough to format it properly and more importantly:
get it to prompt for a numerical input between 0-9999
The other answers have many flaws, because they check that the user didn't input a number outside of the range they want. But what if a user enters something that is not a number? their strategy is broken from the start.
Instead it's better to let go only when we're sure that the user entered a number which lies within the wanted range.
while :; do
read -ep 'Enter server number: ' number
[[ $number =~ ^[[:digit:]]+$ ]] || continue
(( ( (number=(10#$number)) <= 9999 ) && number >= 0 )) || continue
# Here I'm sure that number is a valid number in the range 0..9999
# So let's break the infinite loop!
break
done
The regex [[ $number =~ ^[[:digit:]]+$ ]] makes sure that the user only entered digits.
The clumsy (number=(10#$number)) part is here so that if the user enters a number that starts with a 0, bash would try to interpret it in radix 8 and we'd get a wrong result (e.g., if the user enters 010) and even an error in the case when a user enters, e.g., 09 (try it without this guard).
If you only want to prompt once and exit when the user inputs invalid terms, you have the logic:
read -ep 'Enter server number: ' number
[[ $number =~ ^[[:digit:]]+$ ]] || exit 1
(( ( (number=(10#$number)) <= 9999 ) && number >= 0 )) || exit 1
# Here I'm sure that number is a valid number in the range 0..9999
If you want to explain to the user why the script exited, you can use a die function as:
die() {
(($#)) && printf >&2 '%s\n' "$#"
exit 1
}
read -ep 'Enter server number: ' number
[[ $number =~ ^[[:digit:]]+$ ]] ||
die '*** Error: you should have entered a number'
(( ( (number=(10#$number)) <= 9999 ) && number >= 0 )) ||
die '*** Error, number not in range 0..9999'
# Here I'm sure that number is a valid number in the range 0..9999
<--edit-->
if all you want is the mechanic for prompting, try this:
echo -n "Enter server number:"
read userinput
then run validation checks on the input like this:
if [[ $userinput -lt 0 || $userinput -gt 9999 ]] # checks that the input is within the desired range
then
echo "Input outside acceptable range."
else
# insert your grep and ping stuff here
fi
<--end edit-->
on first read, i thought your problem sounded ideal for a wrapper script, so i was going to suggest this:
$ cat wrapper.sh
#!/usr/bin/bash
userinput=$1
if [[ $# != 1 ]] # checks that number of inputs is exactly one
then
echo "Too many inputs."
exit 2
elif [[ $userinput -lt 0 || $userinput -gt 9999 ]] # checks that the input is within the desired range
then
echo "Input outside acceptable range."
exit 3
fi
output=`d "$userinput"`
ping_address=`grep -E -o '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' <("$output")`
ping "$ping_address"
then call the script with like this:
$ wrapper.sh 1243
If you just want a number between two values, you can test their values:
read x
while [[ $x -lt 0 || $x -gt 9999 ]]; do
echo "bad value for x"
read x
done
echo "x=$x"
The read command doesn't prompt itself. Use a normal echo before to actually display a prompt. Use echo -n to not add a newline.

Resources