Writing a program in bash shell in UNIX - arrays

I have to write a program that lets the user enter in as many numbers as he wants and determine which is the largest, smallest, what the sum is, and the average of all the numbers entered. Am I forced to use arrays to do this or is there another way? If I have to use an array, can someone help me out with an example of how I should be approaching this question? Thanks

You do not need an array. Just keep the largest and smallest number so far, the count of numbers and the sum. The average is simply sum/count.
To read the input, you can use read in a while loop.

Simple straight forward attempt, with some issues:
#!/bin/bash
echo "Enter some numbers separated by spaces"
read numbers
# Initialise min and max with first number in sequence
for i in $numbers; do
min=$i
max=$i
break
done
total=0
count=0
for i in $numbers; do
total=$((total+i))
count=$((count+1))
if test $i -lt $min; then min=$i; fi
if test $i -gt $max; then max=$i; fi
done
echo "Total: $total"
echo "Avg: $((total/count))"
echo "Min: $min"
echo "Max: $max"
Also tested with /bin/sh, so you don't actually need bash, which is a much larger shell. Also note that this only works with integers, and average is truncated (not rounded).
For floating point, you could use bc. But instead of dropping into a different interpreter multiple times, why not just write it in something a bit more suitable to the problem, such as python or perl, eg in python:
import sys
from functools import partial
sum = partial(reduce, lambda x, y: x+y)
avg = lambda l: sum(l)/len(l)
numbers = sys.stdin.readline()
numbers = [float(x) for x in numbers.split()]
print "Sum: " + str(sum(numbers))
print "Avg: " + str(avg(numbers))
print "Min: " + str(min(numbers))
print "Max: " + str(max(numbers))
You could embed it in bash using a here document, see this question: How to pipe a here-document through a command and capture the result into a variable?

Related

Bash: Removing empty or missing entries in associative array

I have an associative array, as below, with helpful suggestions from Stackoverflow community to build the arrays and have them behave in the order values are defined. i.e. Disks go into 'disks_order' in the order they are defined.
Everything is working well, until we encounter something that we don't expect. For example, if an entry is empty (noticed the line commented out). This throws everything out:
#!/bin/bash
localEndPoint="/tmp/test/"
declare -A disks
disks=([0-UUID]=a6506a2c844b40cc [0-MountPoint]='/media/user/SomePath/')
disks+=([1-UUID]=a1f369ab35c5a22e [1-MountPoint]='/media/user/SomeOtherPath/')
disks+=([2-UUID]=6fd9ad56f4b94648 [2-MountPoint]='/media/user/AnotherOne/')
##disks+=([3-UUID]=aae8222d2ac131b0 [3-MountPoint]='/media/user/YetAnother/') ## notice this line is commented out
disks+=([4-UUID]=0 [4-MountPoint]='/home/user/')
disks+=([5-UUID]=0 [5-MountPoint]='/home/user2/')
# build array so as to reflect the way it was defined above
# find total number of keys used and then use that for the division calculation (https://stackoverflow.com/questions/63532910)
declare -A keycount; for i in "${!disks[#]}"; do keycount[${i//[0-9]-/}]=""; done
totalDisks=$((${#disks[*]} / ${#keycount[#]}))
x=1; while [[ $x -le $totalDisks ]]; do disks_order+="$(( x-1 )) "; x=$(( $x + 1 )); done # 'disks_order' array is used to process which disks are handled in what order
# list everything
for i in ${disks_order[*]}; do
diskLabel=$( basename "${disks[$i-MountPoint]}" )
printf "Point $(( i+1 ))"
if [[ "${disks[$i-UUID]}" == 0 ]]; then
printf ": Path "
else
printf ": Disk "
fi
printf "${disks[$i-MountPoint]} to ${localEndPoint}${diskLabel}/\n"
done
Actual output
Point 1: Disk /media/user/SomePath/ to /tmp/test/SomePath/
Point 2: Disk /media/user/SomeOtherPath/ to /tmp/test/SomeOtherPath/
Point 3: Disk /media/user/AnotherOne/ to /tmp/test/AnotherOne/
Point 4: Disk to /tmp/test//
Point 5: Path /home/user/ to /tmp/test/user/
(notice the empty line for Point 4, and loss of /home/user2)
Desired Output
Point 1: Disk /media/user/SomePath/ to /tmp/test/SomePath/
Point 2: Disk /media/user/SomeOtherPath/ to /tmp/test/SomeOtherPath/
Point 3: Disk /media/user/AnotherOne/ to /tmp/test/AnotherOne/
Point 4: Path /home/user/ to /tmp/test/user/
Point 5: Path /home/user2/ to /tmp/test/user2/
So. How can I remove empty entries disks_order?
I suspect we need to revisit the solution from this answer re "counting the total number of keys in an array"?
i.e.
declare -A keycount; for i in "${!disks[#]}"; do keycount[${i//[0-9]-/}]=""; done
totalDisks=$((${#disks[*]} / ${#keycount[#]}))
x=1; while [[ $x -le $totalDisks ]]; do disks_order+="$(( x-1 )) "; x=$(( $x + 1 )); done
Edit: totalDisks is needed to display total number of entries in array, to be used later in the script.
Add a function to add discs that would number them auto....
declare -A disks
disks_add() {
local n="$((${#disks[#]}/2))"
disks+=([$n-UUID]="$1" [$n-MountPoint]="$2")
}
disks_add a6506a2c844b40cc '/media/user/SomePath/'
Remember to check your scripts with https://shellcheck.net
But I do not see any advantages in your design, just you add complexity with those totalDisks and disk_order stuff. I would just use two arrays:
disk_uuids=() disk_mountpoints=()
add_disk() {
# I am so lazy to type...
disk_uuids+=("$1")
disk_mountpoints+=("$2")
}
add_disk a6506a2c844b40cc '/media/user/SomePath/'
# Who cares about indexes??
add_disk a1f369ab35c5a22e '/media/user/SomeOtherPath/'
add_disk 6fd9ad56f4b94648 '/media/user/AnotherOne/'
for ((i=0;i<${#disk_uuids[#]};++i)); do
echo "Wow, so easy no sorting..."
printf "%s\t%s\t%s\n" "$i" "${disk_uuids[i]}" "${disk_mountpoints[i]}"
done

BASH scripting: Organizing output of a nested loop into a table

I have wasted so much of my time with this, I hope someone can help me. I am editing a script that is used to send values to an executable and then scrubs the output from the executable for tabulation. I have created two arrays that are filled with user inputted ranges, these ranges are then used to create a nested loop that I have put into a function (since I need to create 4 files from the output depending on another value). I admit that my code is abysmal, but it does the primary thing and that is obtaining the data I need and puts into the correct file. All I am trying to do is to get it to actually make a table with row and column labels, I just can't understand why this is so difficult.
This is the problematic area:
# Function to loop user inputted ranges in order to generate report data.
function repeat()
{
printf "%22s" 'density (10^18 m^-3)'
for a in "${density_array[#]}" # for loop to list density values in the range set by the user.
do
printf "%13s" "$a"
done
echo -e "\n" 'speed (m/s)'
#printf "%s\n" "${speed_array[#]}"
for i in "${speed_array[#]}"
do
echo "$i"
for j in "${density_array[#]}"
do
echo $j > SCATINPUT # generates a temporary file named SCATINPUT, with density value as first line.
echo $temp >> SCATINPUT # appends a new line with temperature value to SCATINPUT file.
echo $i >> SCATINPUT # appends a new line with speed value to SCATINPUT file.
echo $1 >> SCATINPUT # appends a new line with rate type from argument to SCATINPUT file.
# pipes contents of SCATINPUT file to executable, extracts value from STDOUT to RATE variable.
RATE=`path_of_executable < SCATINPUT | awk '/0\./'`
RATEF=$(printf "%.4e" $RATE) # converts number in RATE variable to scientific notation with 4 digits after decimal and sets RATEF variable.
echo -ne "\t$RATEF"
rm -f SCATINPUT # quietly deletes SCATINPUT file.
done
done
}
I am getting this output in a file:
density (10^18 m^-3) 2.0000e+00 4.0000e+00 6.0000e+00
speed (m/s)
8.0000e+06
7.6164e+04 1.4849e+05 2.1936e+059.0000e+06
5.7701e+04 1.1249e+05 1.6619e+051.0000e+07
4.3469e+04 8.4747e+04 1.2520e+051.1000e+07
3.3078e+04 6.4488e+04 9.5269e+041.2000e+07
2.5588e+04 4.9886e+04 7.3697e+04
But it should be this:
density (10^18 m^-3) 2.0000e+00 4.0000e+00 6.0000e+00
speed (m/s)
8.0000e+06 7.6164e+04 1.4849e+05 2.1936e+05
9.0000e+06 5.7701e+04 1.1249e+05 1.6619e+05
1.0000e+07 4.3469e+04 8.4747e+04 1.2520e+05
1.1000e+07 3.3078e+04 6.4488e+04 9.5269e+04
1.2000e+07 2.5588e+04 4.9886e+04 7.3697e+04
The general idea would be to replace the echo commands with comparable printf commands with formats that match the formats used to print the first/header line ...
Start by replacing echo "$i" with printf "%22s" "$i" => this should leave the cursor on the same line as $i and lined up under the 2.0000e+00
After you finish the j loop and before getting the next i do printf "\n" => this should move the cursor to the next line and ready for the next printf "%22s" "$i".
That should get you started.
If things don't line up quite right then consider replacing the echo -ne "\tRATEF" with a printf "%#s" "$RATEF" (adjust the number # to line up output as desired).

Random element from an array bigger than 32767 in bash

Having:
mapfile -t words < <( head -10000 /usr/share/dict/words)
echo "${#words[#]}" #10000
r=$(( $RANDOM % ${#words[#]} ))
echo "$r ${words[$r]}"
This select a random word from the array of 10k words.
But having the array bigger as 32767 (e.g. whole file 200k+ words), it stops work because the $RANDOM is only up to 32767. From man bash:
Each time this parameter is referenced, a random integer between 0 and 32767 is generated.
mapfile -t words < /usr/share/dict/words
echo "${#words[#]}" # 235886
r=$(( $RANDOM % ${#words[#]} )) #how to change this?
echo "$r ${words[$r]}"
Don't want use some perl like perl -plE 's/.*/int(rand()*$_)/e', not every system have perl installed. Looking for the simplest possible solution - and also don't care about the true randomness - it isn't for cryptography. :)
One possible solution is to do some maths with the outcome of $RANDOM:
big_random=`expr $RANDOM \* 32767 + $RANDOM`
Another is to use $RANDOM once to pick a block of the input file, then $RANDOM again to pick a line from within that block.
Note that $RANDOM doesn't allow you to specify a range. % gives a non-uniform result. Further discussion at: How to generate random number in Bash?
As an aside, it doesn't seem particularly wise to read the whole of words into memory. Unless you'll be doing a lot of repeat access to this data structure, consider trying to do this without slurping up the whole file at once.
If shuf is available on your system...
r=$(shuf -i 0-${#words[#]} -n 1)
If not, you could use $RANDOM several times and concatenate the results to obtain a number with enough digits to cover your needs. You should concatenate, not add, as adding random numbers will not produce an even distribution (just like throwing two random dies will produce a total of 7 more often than a total of 1).
For instance :
printf -v r1 %05d $RANDOM
printf -v r2 %05d $RANDOM
printf -v r3 %05d $RANDOM
r4=${r1:1}${r2:1}${r3:1}
r=$(( $r4 % ${#words[#]} ))
The printf statements are used to make sure leading zeros are kept ; the -v option is a hidden gem that allows a variable to be assigned the value (which can, among other things, allow the use of eval to be avoided in many useful real-life cases). The first digit in each of r1, r2 and r3 is stripped because it can only be 0, 1, 2 or 3.
The accepted answer will get you ten digits, but for each five-digit prefix, the last five digits may only be in the range 00000-32767.
The number 1234567890, for example, is not a possibility because 67890 > 32767.
That may be fine. Personally I find this option a bit nicer. It gives you numbers 0-1073676289 with no gaps.
big_random=$(expr $RANDOM \* $RANDOM)

Bash - cut a text file into columns, slice strings and store value into arrays

I would like some advice on some code.
I want to write a small script that will take an input file of this format
$cat filename.txt
111222233334444555666661112222AAAA
2222333445556612323244455445454545
2334556345643534505435345353453453
(and so on)
It will be called as : script inputfile X (where X is the number of slices you want to do)
I want the script to read the file and column-ize the slices, depending on user input, ie if he gave input 1,2 for the first slice, 3,4 for the second, the output would look like this:
#Here the first slice starts on the second digit, and length = 2 digits
#Here the second slice starts on the 3th digit and legth=4 digits
111 1222
222 2333
233 3455
This is what i have so far, but i only get the outputs of the first slicing arranged in a line, any advice please?
$./columncut filename.txt 2
#Initialize arrays
for ((i=1 ; i <= $2; i++)); do
echo "Enter starting digit of $i string"; read a[i]
echo "Enter length in digits of $i string"; read b[i]
done
#Skim through file, slice strings
while read line
do
for i in "${!a[#]}"; do
str[i]=${line:${a[i]}:${b[i]}}
done
for i in "${!str[#]}"; do
echo -n "$i "
done
done <$1
I am unaware if there's an easier to do a job like this, perhaps with awk? Any help would be much appreciated.
#usage: bash slice.sh d.txt "1-2" "4-8" "10-20"
#column postions 1-2, 4-8 and 10-20 printed.
#Note that it is not length but col position.
inf=$1 # source file
shift # arg1 is used up for file discard it.
while read -r line
do
for fmt #iterate over the arguments
do
slice=`echo $line | cut -c $fmt` # generate one slice
echo -n "$slice " # oupt with two blanks, but no newline
done
echo "" # Now give the newline
done < "$inf"
Sample run:
bash slice.sh d.txt "1-2" "4-8" "10-15"
11 22223 334444
22 23334 555661
23 45563 564353
Probably it is not very difficult to store all these generated slices in array.

How do i create array and put value on it - BASH script

ahhh array and loops my weakest links. I was trying to create array depending on user input so
printf "%s\n" "how may array you want"
read value
after this i will ask what value user want to put on a array(this is the bit im stuck on)
i=1
while [ $i -le $value ]
do
echo "what value you want to put in array $i"
read number
echo $number >> array.db
i=$(( i+1 ))
echo
done
although this method works(i think) but i'm not too sure if i'm actually creating an array and putting value to that array.
you can expand arrays in bash dynamically. you can use this snippet
a=(); a[${#a[#]}]=${number}; echo ${a[#]}
The first statement defines an empty array. with the second (which you can use in your while loop) you insert a value at last elment position + 1, due to ${#a[#]} represents the length of a. the third statement just prints all elements in the array.

Resources