I am trying to learn bash so I was working on a "guess the number" type game. I am at the point of trying to generate random numbers based on the user's input for lower and upper bounds. But some test cases seem to create numbers that don't make sense; some being outside my bounds and others not being what I would expect.
Here is my program
echo "Enter the lower bound: "
echo -n "> "
read lowerBound
while [ "$lowerBound" -lt 0 ]
do
echo "Lower bound must be >= 0. Please try again: "
echo -n "> "
read lowerBound
done
echo
echo "Enter the upper bound: "
echo -n "> "
read upperBound
lowerLimit=$(($lowerBound + 1))
while [ "$upperBound" -lt "$lowerLimit" ]
do
echo "Upper bound must be larger than lower bound. Please try again: "
echo -n "> "
read upperBound
done
echo
randNum=$(( $RANDOM % ( ($upperBound - $lowerBound) + 1 ) ))
echo "RANDOM=$RANDOM randNum=$randNum"
Here is an output that produced unexpected results. I would expect 6: (9237 % (10-1+1) = 7 but output is 5)
Enter the lower bound:
> 1
Enter the upper bound:
> 10
RANDOM=9237 randNum=5
Finally here is some output where the generated number lies outside my bounds. I understand this from doing the math myself, but thought this equation was supposed to generate random numbers in a range. Here 26921 % (126-123+1) = 9237. Most likely just a coincidence that 9237 appeared in subsequent runs.
Enter the lower bound:
> 123
Enter the upper bound:
> 126
RANDOM=26921 randNum=0
I'm not sure if my equation is wrong or if it is something I am doing wrong in Bash. Any pointers would be appreciated.
Note: I am not new to programming, just new to Bash.
Each reference of $RANDOM generates a new number.
You can see this with something as simple as:
$ echo "$RANDOM : $RANDOM : $RANDOM"
297 : 20330 : 14461
In your code you get one $RANDOM value when you calculate randNum, and a second (and likely different) $RANDOM value in the echo; net result is randNum=5 was not generated based on $RANDOM=9237.
If you want to reference the same $RANDOM value more than once you should first store it in a variable and then (re)use said variable as needed, eg:
ranx=$RANDOM
randNum=$(( $ranx % ( ($upperBound - $lowerBound) + 1 ) ))
echo "RANDOM=$ranx randNum=$randNum"
Related
Background
Beginner here trying to learn some Bash basics.
My question is my attempt at modifying an existing textbook example to include simple input validation. The validation works if one or more inputs are inappropriate but when all three inputs are appropriate, I instead get a syntax error warning and then an answer.
Code is at the end, but below are my thoughts. Appreciate any corrections wherever they are wrong.
Much thanks.
Thoughts
I have three conditions to check for. If any one of them fails, a message is displayed and the program terminates. This works. However, if all 3 conditions are met, then I receive a syntax error.
I thought the error might be related to expansion so I manually ran the commands and supplied hard inputs using the command line. E.g. bc <<< "0.5 > 0" and these seemed to work as intended.
Subsequently, it seems that my problems only arise when I involve the $interest variable with its decimal points. However, I used BC because my understanding is that Bash only does Integers. What else have I missed out?
Code
# loan-calc: script to calculate monthly loan payments
# formulae may be mathematically wrong, example copied from textbook
# reference only for bash scripting, not math
PROGNAME="${0##*/}"
usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
PRINCIPAL is the amount of the loan.
INTEREST is the APR as a number (7% = 0.07)
MONTHS is the length of the loan's term.
EOF
}
read -p "Enter principal amount > " principal
read -p "Enter interest rate (E.g. 7.5% = 0.075) > " interest
read -p "Enter term length in months > " months
# Principal, interest rate, and months must be more than 0.
if (( "$principal <= 0" | bc )) || (( "$months <= 0" | bc )) || (( "$interest <= 0" | bc )); then
usage
exit 1
fi
result=$(bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
EOF
)
printf "%0.2f\n" $result
Shell Arithmetic
(( 1 | bc )) does not pipe 1 into bc. When you are evaluating expressions inside of (( expression )), | is the bitwise OR operator, not a pipe.
(( bc )) evaluates to 1 every time, so all of your conditional tests are just OR'ing the number with 1, not piping the number into bc.
Your expression inside of the parentheses should be the output from echoing the mathematical string into bc using a pipe, e.g. (( $(echo "$variable <= 0"| bc) )).
This can be wrapped in a named function so the if statement is more readable.
notValid() {
(( $(echo "$1 <= 0" | bc) ))
}
# Principal, interest rate, and months must be more than 0.
if notValid "$principal" || notValid "$months" || notValid "$interest"; then
usage
exit 1
fi
New to bash on my course, generally enjoying it but as soon as we've been given some coursework it's thrown a spanner in the works.
The assignment is essentially to take an arbitrary amount of numbers from the user (using the read command), add them up, and return the result.
The previous task included a fixed amount of 10 numbers, for which i wrote:
#!/bin/sh
echo "Please enter 10 numbers"
read num1
read num2
read num3
read num4
read num5
read num6
read num7
read num8
read num9
read num10
result=$((num1 + num2 + num3 + num4 + num5 + num6 + num7 + num8 + num9 + num10))
echo The result is $result
Now this works fine, but I just know I'm making it harder for myself/too long, and seen as the task I'm struggling with is supposed to come from editing that script that takes 10 numbers, clearly im missing something pretty basic to take in numbers from the user & add them together without writing it 10 different times
A little guidance on user input with arbitrary numbers would be great
You can loop over a prompt and sum one number at a time to the total.
total=0
for((i=1; i<=10; i++)); do
read -p "Please enter a number: " -r num
((total+=num))
done
echo "The total is $total"
The "C-style" for loop syntax is a Bash extension; if you need your script to be portable to POSIX sh you can do something like
for i in $(seq 10); do
:
or if you can't rely on seq being installed, the age-old fugly
i=1
while [ "$i" -le 10 ]; do
:
i=$(expr "$i" + 1)
done
while true
do
if [ $userinput = 1 ];
then
guesses=10
(( answer = RANDOM % 20 ))
read -p "Guess the number between 1-20 if you can $answer : " input
if [ $input != $answer ];
then
(( guesses=guesses-1 ))
echo "Wrong answer! You got ${guesses} left!"
else
echo "Correct answer! You had ${guesses} left. Lucky you!"
read -p "${name}, would you like to continue playing or not [Yes/No]? " decide
if [ $decide = "Yes" ];
then
continue
else
echo -e "${Red}Bye bye!"
Example:
guess=10
User inputs 2 guesses then program has to minus those 2 guesses from total of 10 guesses, in that case, 10-2=8 guesses left. How to do this?
The only necessary change is moving guesses=10 out of your loop, such that it's run only once (when your script is starting).
As for best-practice decrement forms, a terser bash-only approach (albeit no more or less valid than your existing implementation) would look like:
(( guesses-- ))
...whereas a more portable approach (compatible with all POSIX-family shells) is:
guesses=$(( guesses - 1 ))
If you are specifically using the Bash shell, then the let builtin is what you want.
The let builtin is a clean way to perform integer arithmetic in Bash. You should read the output of help let to get a better picture of how it works.
The two lines you circled in your linked picture could be written as follows:
let guesses=10 # Set "guesses" to 10
let guesses-=1 # Decrement "guesses" by 1
Most proficient Bash hackers would write these operations in this manner. You could also use the post-decrement operator.
let guesses-- # Also decrement "guesses" by 1
This may look a little cleaner to you.
Some of you are probably familiar with Project Euler, and I'm currently attempting a few of their problems to teach myself some more bash. They're a bit more mathematical than 'script-y' but it helps with syntax etc.
The problem currently asks me to solve:
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
The code I have looks like so:
#!/bin/bash
i="1"
for i in `seq 1 333`
do
threes[$i]=`calc $i*3` # where 'calc' is a function written in bashrc
#calc actually looks like: calc() {awk "BEGIN { print "$*"} }
let "sumthrees = sumthrees + ${threes[$i]}"
done
for i in `seq 1 199`
do
fives[$i]=`calc $i*5`
let "sumfives = sumfives + ${fives[$i]}"
done
let "ans = $sumfives + $sumthrees"
echo "The sum of all 3 factors is $sumthrees and the sum of all five factors is $sumfives"
echo "The sum of both is $ans"
#So I can repeatedly run the script without bash remembering the variables between executions
unset i
unset fives
unset threes
unset sumfives
unset sumthrees
unset ans
So far I've not gotten the correct answer, but have run out of ideas as to where I'm going wrong. (FYI, the script currently gives me 266333, which I believe is close, but I don't know the answer yet.)
Can anyone spot anything? And for my own learning, if there are more elegant solutions to this that people might like to share that would be great.
EDIT
Thanks for all the answers, super informative. Since there are so many useful answers here I'll accept my favourite as the proper thread answer.
Blue Moon pointed out the actual problem with your logic.
You don't need to store all the threes and fives in arrays because you don't need them later.
You don't need to unset variables at the end of a script if you use ./yourscript or bash script because they'll disappear along with the
shell instance (better to initialize them first in any case).
You don't need awk to do math, bash does that just fine.
seq and let are not the best way to do anything in a bash script.
Here's a straight forward version:
#!/bin/bash
sum=0
for ((i=1; i<1000; i++))
do
if (( i%3 == 0 || i%5 == 0 ))
then
(( sum += i ))
fi
done
echo "$sum"
Your logic is almost right except that there are numbers which divide by both 3 and 5. So you are adding these numbers twice. Hence, you get wrong answer.
Use another loop similar to ones you have and subtract the ones that divide by both 3 and 5 from the result.
A few tips you might find useful:
In bash, you use let to give the shell a hint that a variable should be considered a number. All bash variables are strings, but you can do arithmetic on numerical strings. If I say let i=1 then i is set to 1, but if I say let i="taco" then $i will be 0, because it couldn't be read as a number. You can achieve a small amount of type-safety when doing mathematical work in the shell.
Bash also has $((this)) mechanism for doing math! You can check it out yourself: echo $((2 + 2)) -> 4, and even more relevant to this problem: echo $((6 % 3 == 0)) -> 1
In case you aren't familiar, % divides the first number by the second, and gives back the remainder; when the remainder is 0, it means that the first is divisible by the second! == is a test to see if two things are equal, and for logical tests like this 1 represents true and 0 represents false. So I'm testing if 6 is divisible by 3, which it is, and the value I get back is 1.
The test brackets, [ ... ] have a "test for equality" flag, -eq, which you can use to check if a math expression has a certain value (man test for more details)!
$ let i=6
$ echo $((i % 3 == 0 || i % 5 == 0))
1
$ if [ $((i % 3 == 0 || i % 5 == 0)) -eq 1 ]; then echo "yes"; fi
yes
(|| is another logical test - $((a || b)) will be 1 (true) when a is true or b is true).
Finally, instead of doing this for the number 6, you could do it in a for loop and increment a sum variable every time you find a multiple of 3 or 5:
let sum=0
for i in {1..1000}; do
if [ $((i % 3 == 0 || i % 5 == 0)) -eq 1 ]; then
let sum=$((sum + i))
fi
done
echo $sum
And there you'd have a working solution!
Bash has a lot of nice little tricks, (and a lot more mean ugly tricks), but it's worth learning at least a handful of them to make use of it as a scripting tool.
How about creative use of the modulus function & some checks. Then you have just 1 loop.
#!/bin/bash
i=1
while [ $i -lt 1000 ]
do
if [ $(($i % 3)) -eq 0 ] || [ $(($i % 5)) -eq 0 ]
then
sumall=$(($sumall+$i))
fi
i=$(($i+1))
done
echo "The sum of both is $sumall"
Answer: 233168
A different solution:
#!/bin/bash
sum=0
for n in {1..999}; do [ $(((n%5) * (n%3))) -eq 0 ] && sum=$((sum+n)); done
echo $sum
The script loops through all numbers below 1000, tests if the product of the number mod 3 and the number mod 5 is 0 (the product of two numbers can only be zero if one of them is zero). If that is the case, it adds the current number to a sum, which is printed out afterwards.
By the way, if I were you I'd include the definition of the calc function inside the script, to get a self-contained solution that doesn't need your specific configuration.
I'm writing a UNIX script to use the sieve to generate prime numbers. I keep getting a bad modulo division on line 19, and I can't seem to figure out why.
I have tried all kinds of different formatting, not sure what the right way is.
#!bin/bash
read -p "Upper limit? :" answer
theMultiple=2
#populate the array
for ((i=2;i<$answer;i++)); do
sieveArray[$i]=$i
done
#Use Sieve
for ((i=0;i<=${#sieveArray[*]}; i++)); do
if [ $[$(($[${sieveArray[$i]}] % $theMultiple))] -eq 0 ]; then
theMultiple=${sieveArray[$i]}
echo $theMultiple
for ((j=$i;j<${#sieveArray[*]};j++)); do
if [ $[$(($[${sieveArray[$j]}] % $theMultiple))] -eq 0 ]; then
sieveArray[$j]=0
fi
done
fi
done
}
You start filling in your sieveArray at index 2, yet in your main loop you start using it at index zero. The first two elements are probably set to zero by default which causes a division by zero.
You could write it differently with less bash arithmetic and more commands :
#!/bin/bash
limit=$1
sieve="$(seq 2 $limit|sort)"
for n in 2 $(seq 3 2 $limit)
do
sieve="$(comm -23 <(echo "$sieve") <(seq $(($n * $n)) $n $limit|sort))"
done
echo "$sieve"|sort -n
seq is used to generate lists of numbers and multiples.
comm is used to remove the multiples from the sieve variable. As comm expects data sorted un alphabetical order (10 is before 9), the lists of number must be sorted every time.
The for loop is slightly optimized to not include even numbers, except 2.