How to iterate through variables? [duplicate] - bash

This question already has answers here:
How to zero pad a sequence of integers in bash so that all have the same width?
(15 answers)
Closed 5 years ago.
I was using the following in my current script
for x in {07..10}
Trying to pass the start,end variables to the script using
for x in $(seq $1 $2)
Since the sequence starts from 07, and 07 is a file name that I want to read, I cant change the variable to 7, as it happens when using the sequence. Can you please point me in the right direction as I don't have much experience with bash.

Use printf to get the number format you want:
for (( x=7; x<=10; x++ )); do
str=$( printf "%02d" "$x" )
echo filename${str}.txt
done
Results look like this:
$ for (( x=7; x<=10; x++ )); do str=$( printf "%02d" "$x" ); echo filename${str}.txt; done
filename07.txt
filename08.txt
filename09.txt
filename10.txt
Works with variables, too:
$ start="07"
$ end="10"
$ for (( x=$start; x<=$end; x++ )); do str=$( printf "%02d" "$x" ); echo filename${str}.txt; done
filename07.txt
filename08.txt
filename09.txt
filename10.txt

Related

For loop over sequence of large numbers in Bash [duplicate]

This question already has answers here:
Bash command line and input limit
(4 answers)
Closed 4 years ago.
In a Bash script I am using a simple for loop, that looks like:
for i in $(seq 1 1 500); do
echo $i
done
This for loop works fine. However, when I would like to use a sequence of larger numbers (e.g. 10^8 to 10^12), the loop won't seem to start.
for i in $(seq 100000000 1 1000000000000); do
echo $i
done
I cannot imagine, that these numbers are too large to handle. So my question: am I mistaken? Or might there be another problem?
The problem is that $(seq ...) is expanded into a list of words before the loop is executed. So your initial command is something like:
for i in 100000000 100000001 100000002 # all the way up to 1000000000000!
The result is much too long, which is what causes the error.
One possible solution would be to use a different style of loop:
for (( i = 100000000; i <= 1000000000000; i++ )) do
echo "$i"
done
This "C-style" construct uses a termination condition, rather than iterating over a literal list of words.
Portable style, for POSIX shells:
i=100000000
while [ $i -le 1000000000000 ]; do
echo "$i"
i=$(( i + 1 ))
done

BASH comparing Linux version to number? [duplicate]

This question already has answers here:
How to compare two strings in dot separated version format in Bash?
(38 answers)
Closed 4 years ago.
I'm trying to work out how to compare the current openSuSE version number to a preset value.
I've got the current version of the installed OS in $VERSION_ID
I'm now trying to work out how to compare that to '42.3'. So if the value isn't greater than or equal to quit.
if [ ! "$VERSION_ID" -ge 42.3 ]; then
echo "Sorry Bye";
fi
I'm getting:
[: 42.3: integer expression expected But I don't know how to fix that
Any advise please
Thanks
You can use a calculator bc:
if [ $(echo "$VERSION_ID<=42.3" |bc -l) -eq "1" ]; then
echo "Sorry Bye";
fi
Version numbers aren't floating point values; they are .-delimited sequences of integers. 42.27 is newer than 42.3, and 42.2.9 could be a valid version number.
Split the version number into its integer components, and compare them "lexiconumerically":
target=(42 3)
IFS=. read -a v_id <<< "$VERSION_ID"
for ((i=0; i <${#v_id[#]}; i++)); do
if (( ${v_id[i]} == ${target[i]} )); then
continue
fi
if (( ${v_id[i]} < ${target[i]} )); then
echo "version < target"
elif (( ${v_id[i]} > ${target[i]} )); then
echo "version > target"
fi
break
done

UNTIL LOOP in shell for read file [duplicate]

This question already has answers here:
Read file for value, loop until value = $foo?
(3 answers)
Closed 5 years ago.
#!/bin/bash
i=1
cat days.txt | while read days
do
echo $i $days
let i++
done
I want to change while loop into until loop
#!/bin/bash
i=1
until conditions
do
echo $i $days
let i++
done
expected result
Monday
Tuesday
Wednesday
Blah Blah Blah
while read can be written more awkwardly as until ! read:
i=1
until ! read -r days; do
echo "$i $days"
i=$(( i + 1 ))
done < file
while command does something as long as command exits successfully, whereas until command does something until command exits unsuccessfully. The ! is used to negate the exit code of read.

Formatting Output From Shell Script [duplicate]

This question already has answers here:
Echo tab characters in bash script
(10 answers)
Closed 5 years ago.
I am working on a shell script that takes stdin or file as input and prints the averages and medians for rows or columns depending on the arguments.
When calculating the averages for the columns, the output needs to print out the following (tabbed):
My output currently looks like this (no spaces or tabs):
Averages:
92480654263
Medians:
6368974
Is there a way to echo out the averages and medians with tabs so each average and median set align left correctly? Here is a sample of how I am printing out the averages:
echo "Averages:"
while read i
do
sum=0
count=0
mean=0
#Cycle through the numbers in the rows
for num in $i
do
#Perform calculations necessary to determine the average and median
sum=$(($sum + $num))
count=`expr $count + 1`
mean=`expr $sum / $count`
done
echo -n "$mean"
done < $1
man echo:
-e enable interpretation of backslash escapes
If -e is in effect, the following sequences are recognized:
\t horizontal tab
I'd try echo -n -e "$mean\t", didn't test it though.
You should use printf. For instance, this will print a value followed by a tab
printf "%s\t" "$mean"
You can actually print several values separated by tabs if you want by adding arguments :
printf "%s\t" "$mean" "$count"
You can use an array expansion to print several values separated by tabs :
printf "%s\t" "${my_array[#]}"
Among advantages of printf over echo is the availability of flexible formatting strings, and the fact that implementations of printf vary less than those of echo among shells and operating systems.
You could try using column command but it does take additional steps:
echo "Averages:"
while read line
do
sum=0
count=0
mean=0
#Cycle through the numbers in the rows
for num in $line
do
#Perform calculations necessary to determine the average and median
(( sum += num ))
(( count++ ))
(( mean = sum / count ))
done
(( mean == 0 )) && out=$mean || out="$out|$mean"
done < $1
echo "$out" | column -s'|' -t
Above is untested as I do not have the original file, but you should get the idea. I would add that the division will also provide truncated values so not exactly accurate.

Summing row in bash [duplicate]

I am trying to read a file line by line and find the average of the numbers in each line. I am getting the error: expr: non-numeric argument
I have narrowed the problem down to sum=expr $sum + $i, but I'm not sure why the code doesn't work.
while read -a rows
do
for i in "${rows[#]}"
do
sum=`expr $sum + $i`
total=`expr $total + 1`
done
average=`expr $sum / $total`
done < $fileName
The file looks like this (the numbers are separated by tabs):
1 1 1 1 1
9 3 4 5 5
6 7 8 9 7
3 6 8 9 1
3 4 2 1 4
6 4 4 7 7
With some minor corrections, your code runs well:
while read -a rows
do
total=0
sum=0
for i in "${rows[#]}"
do
sum=`expr $sum + $i`
total=`expr $total + 1`
done
average=`expr $sum / $total`
echo $average
done <filename
With the sample input file, the output produced is:
1
5
7
5
2
5
Note that the answers are what they are because expr only does integer arithmetic.
Using sed to preprocess for expr
The above code could be rewritten as:
$ while read row; do expr '(' $(sed 's/ */ + /g' <<<"$row") ')' / $(wc -w<<<$row); done < filename
1
5
7
5
2
5
Using bash's builtin arithmetic capability
expr is archaic. In modern bash:
while read -a rows
do
total=0
sum=0
for i in "${rows[#]}"
do
((sum += $i))
((total++))
done
echo $((sum/total))
done <filename
Using awk for floating point math
Because awk does floating point math, it can provide more accurate results:
$ awk '{s=0; for (i=1;i<=NF;i++)s+=$i; print s/NF;}' filename
1
5.2
7.4
5.4
2.8
5.6
Some variations on the same trick of using the IFS variable.
#!/bin/bash
while read line; do
set -- $line
echo $(( ( $(IFS=+; echo "$*") ) / $# ))
done < rows
echo
while read -a line; do
echo $(( ( $(IFS=+; echo "${line[*]}") ) / ${#line[*]} ))
done < rows
echo
saved_ifs="$IFS"
while read -a line; do
IFS=+
echo $(( ( ${line[*]} ) / ${#line[*]} ))
IFS="$saved_ifs"
done < rows
Others have already pointed out that expr is integer-only, and recommended writing your script in awk instead of shell.
Your system may have a number of tools on it that support arbitrary-precision math, or floats. Two common calculators in shell are bc which follows standard "order of operations", and dc which uses "reverse polish notation".
Either one of these can easily be fed your data such that per-line averages can be produced. For example, using bc:
#!/bin/sh
while read line; do
set - ${line}
c=$#
string=""
for n in $*; do
string+="${string:++}$1"
shift
done
average=$(printf 'scale=4\n(%s) / %d\n' $string $c | bc)
printf "%s // avg=%s\n" "$line" "$average"
done
Of course, the only bc-specific part of this is the format for the notation and the bc itself in the third last line. The same basic thing using dc might look like like this:
#!/bin/sh
while read line; do
set - ${line}
c=$#
string="0"
for n in $*; do
string+=" $1 + "
shift
done
average=$(dc -e "4k $string $c / p")
printf "%s // %s\n" "$line" "$average"
done
Note that my shell supports appending to strings with +=. If yours does not, you can adjust this as you see fit.
In both of these examples, we're printing our output to four decimal places -- with scale=4 in bc, or 4k in dc. We are processing standard input, so if you named these scripts "calc", you might run them with command lines like:
$ ./calc < inputfile.txt
The set command at the beginning of the loop turns the $line variable into positional parameters, like $1, $2, etc. We then process each positional parameter in the for loop, appending everything to a string which will later get fed to the calculator.
Also, you can fake it.
That is, while bash doesn't support floating point numbers, it DOES support multiplication and string manipulation. The following uses NO external tools, yet appears to present decimal averages of your input.
#!/bin/bash
declare -i total
while read line; do
set - ${line}
c=$#
total=0
for n in $*; do
total+="$1"
shift
done
# Move the decimal point over prior to our division...
average=$(($total * 1000 / $c))
# Re-insert the decimal point via string manipulation
average="${average:0:$((${#average} - 3))}.${average:$((${#average} - 3))}"
printf "%s // %0.3f\n" "$line" "$average"
done
The important bits here are:
* declare which tells bash to add to $total with += rather than appending it as if it were a string,
* the two average= assignments, the first of which multiplies $total by 1000, and the second of which splits the result at the thousands column, and
* printf whose format enforces three decimal places of precision in its output.
Of course, input still needs to be integers.
YMMV. I'm not saying this is how you should solve this, just that it's an option. :)
This is a pretty old post, but came up at the top my Google search, so thought I'd share what I came up with:
while read line; do
# Convert each line to an array
ARR=( $line )
# Append each value in the array with a '+' and calculate the sum
# (this causes the last value to have a trailing '+', so it is added to '0')
ARR_SUM=$( echo "${ARR[#]/%/+} 0" | bc -l)
# Divide the sum by the total number of elements in the array
echo "$(( ${ARR_SUM} / ${#ARR[#]} ))"
done < "$filename"

Resources