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

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).

Related

Fill Two Arrays on the fly From stdin File in Bash

I wrote a bash script that reads a file from stdin $1, and needs to read that file line by line within a loop, and based on a condition statement in each iteration, each line tested from the file will feed into one of two new arrays lets say named GOOD array and BAD array. Lastly, I'll display the total elements of each array.
#!/bin/bash
for x in $(cat $1); do
#testing something on x
if [ $? -eq 0 ]; then
#add the current value of x into array called GOOD
else
#add the current value of x into array called BAD
fi
done
echo "Total GOOD elements: ${#GOOD[#]}"
echo "Total BAD elements: ${#BAD[#]}"
What changes should i make to accomplish it?
#!/usr/bin/env bash
# here, we're checking the number of lines more than 5 characters long
# replace with your real test
testMyLine() { (( ${#1} > 5 )); }
good=( ); bad=( )
while IFS= read -r line; do
if testMyLine "$line"; then
good+=( "$line" )
else
bad+=( "$line" )
fi
done <"$1"
echo "Read ${#good[#]} good and ${#bad[#]} bad lines"
Note:
We're using a while read loop to iterate over file contents. This doesn't need to read more than one line into memory at a time (so it won't run out of RAM even with really big files), and it doesn't have unwanted side effects like changing a line containing * to a list of files in the current directory.
We aren't using $?. if foo; then is a much better way to branch on the exit status of foo than foo; if [ $? = 0 ]; then -- in particular, this avoids depending on the value of $? not being changed between when you assign it and when you need it; and it marks foo as "checked", to avoid exiting via set -e or triggering an ERR trap when your boolean returns false.
The use of lower-case variable names is intentional. All-uppercase names are used for shell-builtin variables and names with special meaning to the operating system -- and since defining a regular shell variable overwrites any environment variable with the same name, this convention applies to both types. See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html

Create associative array from grep output

I have a grep output and I'm trying to make an associative array from the output that I get.
Here is my grep output:
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
I want to use that output to define an associative array, like this:
array[123456789101]=devid1234
array[11213141516]=devid5678
Is that possible? I'm new at making arrays. I hope someone could help me in my problem.
Either pipe your grep output to a helper script with a while loop containing a simple "0/1" toggle to read two lines taking the last field of each to fill your array, e.g.
#!/bin/bash
declare -A array
declare -i n=0
arridx=
while read -r label value; do # read 2 fields
if [ "$n" -eq 0 ]
then
arridx="${value:1}" # strip 1st and lst 2 chars
arridx="${arridx:0:(-2)}" # save in arridx (array index)
((n++)) # increment toggle
else
arrval="${value:1}" # strip 1st and lst 2 chars
arrval="${arrval:0:(-2)}" # save in arrval (array value)
array[$arridx]="$arrval" # assign to associative array
n=0 # zero toggle
fi
done
for i in ${!array[#]}; do # output array
echo "array[$i] ${array[$i]}"
done
Or you can use process substitution containing the grep command within the script to do the same thing, e.g.
done < <( your grep command )
You can also add a check under the else clause that if [[ $label =~ DeviceId ]] to validate you are on the right line and catch any variation in the grep output content.
Example Input
$ cat dat/grepout.txt
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
Example Use/Output
$ cat dat/grepout.txt | bash parsegrep2array.sh
array[123456789101] devid1234
array[111213141516] devid5678
Parsing out the values is easy, and once you have them you can certainly use those values to build up an array. The trickiest part comes from the fact that you need to combine input from separate lines. Here is one approach; note that this script is verbose on purpose, to show what's going on; once you see what's happening, you can eliminate most of the output:
so.input
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
so.sh
#!/bin/bash
declare -a hardwareInfo
while [[ 1 ]]; do
# read in two lines of input
# if either line is the last one, we don't have enough input to proceed
read lineA < "${1:-/dev/stdin}"
# if EOF or empty line, exit
if [[ "$lineA" == "" ]]; then break; fi
read lineB < "${1:-/dev/stdin}"
# if EOF or empty line, exit
if [[ "$lineB" == "" ]]; then break; fi
echo "$lineA"
echo "$lineB"
hwsn=$lineA
hwsn=${hwsn//HardwareSerialNumber/}
hwsn=${hwsn//\"/}
hwsn=${hwsn//:/}
hwsn=${hwsn//,/}
echo $hwsn
# some checking could be done here to test that the value is numeric
devid=$lineB
devid=${devid//DeviceId/}
devid=${devid//\"/}
devid=${devid//:/}
devid=${devid//,/}
echo $devid
# some checking could be done here to make sure the value is valid
# populate the array
hardwareInfo[$hwsn]=$devid
done
# spacer, for readability of the output
echo
# display the array; in your script, you would do something different and useful
for key in "${!hardwareInfo[#]}"; do echo $key --- ${hardwareInfo[$key]}; done
cat so.input | ./so.sh
"HardwareSerialNumber": "123456789101",
"DeviceId": "devid1234",
123456789101
devid1234
"HardwareSerialNumber": "111213141516",
"DeviceId": "devid5678",
111213141516
devid5678
111213141516 --- devid5678
123456789101 --- devid1234
I created the input file so.input just for convenience. You would probably pipe your grep output into the bash script, like so:
grep-command | ./so.sh
EDIT #1: There are lots of choices for parsing out the key and value from the strings fed in by grep; the answer from #David C. Rankin shows another way. The best way depends on what you can rely on about the content and structure of the grep output.
There are also several choices for reading two separate lines that are related to each other; David's "toggle" approach is also good, and commonly used; I considered it myself, before going with "read two lines and stop if either is blank".
EDIT #2: I see declare -A in David's answer and in examples on the web; I used declare -a because that's what my version of bash wants (I'm using a Mac). So, just be aware that there can be differences.

shell script : if array value was greater than a number then run a command

i have a a files containing usernames and users sent count mail per line . for example (dont know how many line have ) :
info.txt >
500 example1
40 example2
20 example3
....
..
.
if the number was greater than X , i want to run commands containing the user name and act on user .
getArray() {
users=() # Create array
while IFS= read -r line # Read a line
do
users+=("$line") # Append line to the array
done < "$1"
}
getArray "/root/.myscripts/spam1/info.txt"
# i know this part is incorrect and need help here :
if [ "${users[1$]}" -gt "50" ]
then
echo "${users[2$] has sent ${users[1$]} emails"
fi
please Help
Thanks
Not knowing how many lines of input you have is no reason to use an array. Indeed, it is generally more useful if you assume your input is infinite (an input stream), so reading into an array is impossible. Just read each line and take action if necessary:
#!/bin/sh
while read -r count user; do
if test "$count" -gt 50; then
echo "$user has sent $count emails"
fi
done < /root/.myscripts/spam1/info.txt

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 output the contents of a while read line loop to multiple arrays in bash?

I read the files of a directory and put each file name into an array (SEARCH)
Then I use a loop to go through each file name in the array (SEARCH) and open them up with a while read line loop and read each line into another array (filecount). My problem is its one huge array with 39 lines (each file has 13 lines) and I need it to be 3 seperate arrays, where
filecount1[line1] is the first line from the 1st file and so on. here is my code so far...
typeset -A files
for file in ${SEARCH[#]}; do
while read line; do
files["$file"]+="$line"
done < "$file"
done
So, Thanks Ivan for this example! However I'm not sure I follow how this puts it into a seperate array because with this example wouldnt all the arrays still be named "files"?
If you're just trying to store the file contents into an array:
declare -A contents
for file in "${!SEARCH[#]}"; do
contents["$file"]=$(< $file)
done
If you want to store the individual lines in a array, you can create a pseudo-multi-dimensional array:
declare -A contents
for file in "${!SEARCH[#]}"; do
NR=1
while read -r line; do
contents["$file,$NR"]=$line
(( NR++ ))
done < "$file"
done
for key in "${!contents[#]}"; do
printf "%s\t%s\n" "$key" "${contents["$key"]}"
done
line 6 is
$filecount[$linenum]}="$line"
Seems it is missing a {, right after the $.
Should be:
${filecount[$linenum]}="$line"
If the above is true, then it is trying to run the output as a command.
Line 6 is (after "fixing" it above):
${filecount[$linenum]}="$line"
However ${filecount[$linenum]} is a value and you can't have an assignment on a value.
Should be:
filecount[$linenum]="$line"
Now I'm confused, as in whether the { is actually missing, or } is the actual typo :S :P
btw, bash supports this syntax too
filecount=$((filecount++)) # no need for $ inside ((..)) and use of increment operator ++
This should work:
typeset -A files
for file in ${SEARCH[#]}; do # foreach file
while read line; do # read each line
files["$file"]+="$line" # and place it in a new array
done < "$file" # reading each line from the current file
done
a small test shows it works
# set up
mkdir -p /tmp/test && cd $_
echo "abc" > a
echo "foo" > b
echo "bar" > c
# read files into arrays
typeset -A files
for file in *; do
while read line; do
files["$file"]+="$line"
done < "$file"
done
# print arrays
for file in *; do
echo ${files["$file"]}
done
# same as:
echo ${files[a]} # prints: abc
echo ${files[b]} # prints: foo
echo ${files[c]} # prints: bar

Resources