I am writing a simple bash script to count the number of occurrences of random draws of cards. I store those in an array, and when printing out the results, for every 10 times that card is pulled, I print one single '*' in a sort of Histogram style of output.
Although, I keep receiving this error when compiling on Terminal:
"task1.sh: line 29: % 10 : syntax error: operand expected (error token is "% 10 ")
task1.sh: line 33: % 10: syntax error: operand expected (error token is "% 10")"
Can't seem to figure out why though. Thank you in advance for any help.
#!/bin/bash
randomdraw(){
Suits="Clubs Diamonds Hearts Spades"
suit=($Suits)
Denominations="2 3 4 5 6 7 8 9 10 Jack Queen King Ace"
denomination=($Denominations)
num_suits=${#suit[*]}
num_denominations=${#denomination[*]}
declare -a numoccurences
declare -a suitoccurences
for ((x=0 ; $x<$loopnum ; x=$x+1));
do
(( numoccurences[$(( RANDOM%num_denominations ))]++ ))
(( suitoccurences[$(( RANDOM%num_suits ))]++ ))
done
}
echo "How Many Random Draws?: "
read loopnum
randomdraw loopnum
for ((x=0 ; $x<$num_denominations ; x=$x+1));
do
let "rounder=$(( ${numoccurences[x]} % 10 ))"
if [ $rounder -ge 5 ];
then
let "starnum=$(( $(( ${numoccurences[x]} / 10 )) + 1 ))"
else
let "starnum=$(( ${numoccurences[x]} / 10 ))"
fi
echo "${denomination[x]}: "
for ((k=0 ; $k<$starnum ; k=$k+1));
do
echo "*"
done
done
Your num_denominations array is mostly empty and the
let "rounder=$(( ${numoccurences[x]} % 10 ))"
is evaluated to
let "rounder=$(( % 10 ))"
Print numoccurences and suitoccurences before asking for loop number for debugging.
You should try to be consistent in the way you write arithmetic expressions in bash. You don't need to use $ to introduce a variable inside an arithmetic expression. And you don't need to use ${array[idx]} either. There's no reason to use let if you have arithmetic evaluation, either. So instead of
let "rounder=$(( ${numoccurences[x]} % 10 ))"
You could write:
(( rounder = numoccurences[x] % 10 ))
These don't quite do the same thing. In the first one, ${numoccurences[x]} will be substituted with nothing if numoccurrences doesn't have a value corresponding to the key $x. In the second one, numoccurrence[x] will be replaced by 0, which is what you actually want. (That has nothing to do with the unnecessary let, since the $((...)) arithmetic expression is evaluated before let is run.)
There are many other places in that script where you would be well advised to simplify your style. For example,
let "starnum=$(( $(( ${numoccurences[x]} / 10 )) + 1 ))"
would be more robust and more readable as
(( starnum = numoccurences[x] / 10 + 1 ))
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
Taking count from file, say if count = 5, I want to print 5 variables. i.e. A B C D E.
If count = 2, Print 2 variables A B, etc.
I have tried using the ASCII values but couldn't go through it.
for i in {1..5}; do
count=5; a=0;
printf "\x$(printf %x '65+$a')";
count=count+1;
done
if count = 5, I want to print 5 variables. i.e. A B C D E. If count = 2, Print 2 variables A B, etc.
Here's a program that matches your style that does what you are looking for:
a=0
for i in {1..5}; do
printf "\x$(printf %x $(( 65 + a )) )";
a=$((a+1));
done
The first thing to note is that in order to do math in bash, you'll need to use the $(( )) operation. Above, you can see I replaced you '65+$a' with $(( 65 + a )) . That's the big news that you need to get math done.
There were a couple of other little issues, but you were stuck on the $(()) stuff so they weren't clear yet. Incidentally, the 'a' variable can be completely removed from the program to just use the 'i' variable like this:
for i in {1..5}; do
printf "\x$(printf %x $(( 64 + i )) )";
done
I had to change the constant to 64 since we are now counting starting at 1.
The {1..5} expression is a good short cut for 1 2 3 4 5, but you won't be able to put a variable into it. So, if you need to add a count variable back in, consider using the seq program instead like this:
count=$1
for i in $(seq 1 $count); do
printf "\x$(printf %x $(( 64 + i )) )";
done
Note that $() is different than the math operator $(()). $() runs a subcommand returning the results.
method 1: simple brace expansion
#!/bin/bash
# generate a lookup table
vars=( - $(echo {A..Z}) )
# use the elements
for i in {1..5}; do
echo ${vars[$i]}
done
{A..Z} generates 26 strings: A, B, ..., Z
which get stored in an array variable by vars=(...)
we prepend a - that we'll ignore
we can then do 1-based indexing into the array
limited to 26 variables (or whatever range we choose)
method 2: multiple brace expansion to generate arbitrary long variables
#!/bin/bash
if [[ ! $1 =~ ^[0-9]+$ ]]; then
echo "Usage: $0 count"
exit
fi
cmd='{A..Z}'
for (( i=$1; i>26; i=i/26 )); do
cmd="${A..Z}$cmd"
done
vars=( $(eval echo $cmd) )
for (( i=0; i<$1; i++ )); do
echo ${vars[$i]}
done
i/26 does integer division (throws away the remainder)
I'm lazy and generate "more than enough" variables rather than attempting to calculate how many is "exactly enough"
{a..b}{a..b}{a..b} becomes aaa aab aba abb baa bab bba bbb
using eval lets us do the brace expansion without knowing in advance how many sets are needed
Sample output:
$ mkvar.sh 10000 |fmt -64 | tail -5
ORY ORZ OSA OSB OSC OSD OSE OSF OSG OSH OSI OSJ OSK OSL OSM
OSN OSO OSP OSQ OSR OSS OST OSU OSV OSW OSX OSY OSZ OTA OTB
OTC OTD OTE OTF OTG OTH OTI OTJ OTK OTL OTM OTN OTO OTP OTQ
OTR OTS OTT OTU OTV OTW OTX OTY OTZ OUA OUB OUC OUD OUE OUF
OUG OUH OUI OUJ OUK OUL OUM OUN OUO OUP
In Mac terminal, I would like to round a large number.
For example,
At 10^13th place:
1234567812345678 --> 1230000000000000
Or at 10^12th place:
1234567812345678 --> 1235000000000000
So I would like to specify the place, and then get the rounded number.
How do I do this?
You can use arithmetic expansion:
$ val=1234567812345678
$ echo $(( ${val: -13:1} < 5 ? val - val % 10**13 : val - val % 10**13 + 10**13 ))
1230000000000000
$ echo $(( ${val: -12:1} < 5 ? val - val % 10**12 : val - val % 10**12 + 10**12 ))
1235000000000000
This checks if the most significant removed digit is 5 or greater, and if it is, the last significant unremoved digit is increased by one; then we subtract the division remainder from the (potentially modified) initial value.
If you don't want to have to write it this way, you can wrap it in a little function:
round () {
echo $(( ${1: -$2:1} < 5 ? $1 - $1 % 10**$2 : $1 - $1 % 10**$2 + 10**$2 ))
}
which can then be used like this:
$ round "$val" 13
1230000000000000
$ round "$val" 12
1235000000000000
Notice that quoting $val isn't strictly necessary here, it's just a good habit.
If the one-liner is too cryptic, this is a more readable version of the same:
round () {
local rounded=$(( $1 - $1 % 10**$2 )) # Truncate
# Check if most significant removed digit is >= 5
if (( ${1: -$2:1} >= 5 )); then
(( rounded += 10**$2 ))
fi
echo $rounded
}
Apart from arithmetic expansion, this also uses parameter expansion to get a substring: ${1: -$2:1} stands for "take $1, count $2 from the back, take one character". There has to be a space before -$2 (or is has to be in parentheses) because otherwise it would be interpreted as a different expansion, checking if $1 is unset or null, which we don't want.
awk's [s]printf function can do rounding for you, within the limits of double-precision floating-point arithmetic:
$ for p in 13 12; do
awk -v p="$p" '{ n = sprintf("%.0f", $0 / 10^p); print n * 10^p }' <<<1234567812345678
done
1230000000000000
1235000000000000
For a pure bash implementation, see Benjamin W.'s helpful answer.
Actually, if you want to round to n significant digits you might be best served by mixing up traditional math and strings.
Serious debugging is left to the student, but this is what I quickly came up with for bash shell and hope MAC is close enough:
function rounder
{
local value=$1;
local digits=${2:-3};
local zeros="$( eval "printf '0%.0s' {1..$digits}" )"; #proper zeros
# a bit of shell magic that repats the '0' $digits times.
if (( value > 1$zeros )); then
# large enough to require rounding
local length=${#value};
local digits_1=$(( $digits + 1 )); #digits + 1
local tval="${value:0:$digits_1}"; #leading digits, plus one
tval=$(( $tval + 5 )); #half-add
local tlength=${#tval}; #check if carried a digit
local zerox="";
if (( tlength > length )); then
zerox="0";
fi
value="${tval:0:$digits}${zeros:0:$((length-$digits))}$zerox";
fi
echo "$value";
}
See how this can be done much shorter, but that's another exercise for the student.
Avoiding floating point math due to the inherit problems within.
All sorts of special cases, like negative numbers, are not covered.
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.
My simple bash program is giving the error stated in the title.
#!/bin/sh
num1=$1
whatOperator=$2
num2=$3
echo Answer: $(($num1$whatOperator$num2))
I have tried adding 10#
echo Answer: $((10#$num1$whatOperator$num2))
as said to do when I have looked this up, but then it gives this error:
value too great for base (error token is "10#3ApplicationsDesktop")
I was wondering if anybody knows what my problem was and how to fix it!
Try adding a space between:
echo "Answer: $(($num1 $whatOperator $num2))"
Only integers are allowed. And the operators you can use are:
** exponentiation
*, /, % multiplication, division, remainder
+, - addition, subtraction
<<, >> left and right bitwise shifts
<=, >=, <, > comparison
==, != equality, inequality
& bitwise AND
^ bitwise XOR
| bitwise OR
&& logical AND
|| logical OR
See help let.
I managed to solve my problem but, I don't know why it is fixed.
calc 3 * 3
gives an error but if I enter
calc 3*3
it works perfectly.
Because * expands as a glob pattern (try echo *) and 3*3 did not match any file so it stayed the way it is literally that your code worked. You should quote glob patterns:
calc 3 '*' 3
calc 3 "*" 3
calc 3 \* 3
calc 3 $'*' 3
I managed to solve my problem but, I don't know why it is fixed.
calc 3 * 3
gives an error but if I enter
calc 3*3
it works perfectly.
Even though I solved the error I was wondering if anybody knows why it is fixed.
The 10# should be added before $num1 and $num2. 10# is useful in case where one of your integers starts with a 0 (example: 10#012 = 12, instead of 012 = 10).
But I think that is not your problem: Looking at the error message you gave, I am pretty sure variables whatOperator and num2 are not what you expect.
Try adding set -xv at the beginning of the script and set +xv at the end ; or echo "whatOperator=$whatOperator. and num2=$num2." (the dots used to delimit the string, to easily see whether there is a line break). I predict whatOperator=Applications. and num2=Desktop.
I would review the call of the script.
Expanding on #konsolebox's answer, the following bash function might prove useful:
function [[[ () {
lhs=$(printf '%07.3f' "$1"); lhs=${lhs/./}
rhs=$(printf '%07.3f' "$3"); rhs=${rhs/./}
case "$2" in
-lt) return $(( 10#$lhs < 10#$rhs )) ;;
-le) return $(( 10#$lhs <= 10#$rhs )) ;;
-eq) return $(( 10#$lhs == 10#$rhs )) ;;
-ge) return $(( 10#$lhs >= 10#$rhs )) ;;
-gt) return $(( 10#$lhs > 10#$rhs )) ;;
esac
}
Usage might go something like:
n=0.83
if [[[ $n -gt 1 ]]]; then
# do something
fi
Note that the trailing ]]] is ignored.