Notes

Reading multiple lines of input from Bash terminal

Edit on GitHub

Bash Scripting
3 minutes

This came up when i was doing HackerRank challenges for Bash. The challenge called Compute the Average had the following problem statement:

Given N integers, compute their average, rounded to three decimal places. The first line contains an integer, N. Each of the following N lines contains a single integer.

My first attempt was to use read with the -r flag in a while loop to read all lines one by one and ignore the \ character used to break up lines.

 1read TOTAL_INTEGERS
 2SUM=0
 3
 4# loops through all args, add them
 5while read -r line
 6do
 7    INT="$line"
 8    $(( SUM += INT ))
 9done
10
11# calculate the average and pipe it to `bc` to get a floating point value
12AVERAGE=$(echo "$SUM/$TOTAL_INTEGERS" | bc -l)
13
14# print average rounded off to three decimal places
15printf "%.3f" $AVERAGE

This failed one of the tests where it turns out it wasn’t adding the last argument. Turns out that the read command fails when the input is not terminated with a newline.

If there are some characters after the last line in the file (or to put it differently, if the last line is not terminated by a newline character), then read will read it but return false, leaving the broken partial line in the read variable(s). You can add a logical OR to the while test ref

So instead of while read -r line, do while read -r line || [[ -n $line ]]

 1read TOTAL_INTEGERS
 2SUM=0
 3
 4# loops through all args, add them
 5# while loops will run till we have complete lines of input
 6# OR 
 7# when `-n` delimiter is reached (in this case the last value for $line) 
 8while read -r line || [[ -n $line ]]
 9do
10    INT="$line"
11    $(( SUM += INT ))
12done
13
14# calculate the average and pipe it to `bc` to get a floating point value
15AVERAGE=$(echo "$SUM/$TOTAL_INTEGERS" | bc -l)
16
17# print average rounded off to three decimal places
18printf "%.3f" $AVERAGE
-r
  If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not then be used as a line continuation.
-n nchars
  read returns after reading nchars characters rather than waiting for a complete line of input, but honors a delimiter if fewer than nchars characters are read before the delimiter.

This worked as expected, but exceeded time limit (1s) by HackerRank. On to the next attempt using readarray ..

readarray Read lines from the standard input into the indexed array variable array, or from file descriptor fd if the -u option is supplied.

 1read TOTAL_INTEGERS
 2SUM=0
 3
 4readarray ARGS
 5
 6# loops through all args, add them
 7for (( i = 0; i < TOTAL_INTEGERS; i++ ))
 8do 
 9    $(( SUM += ARGS[i] ))
10done
11
12# calculate the average and pipe it to `bc` to get a floating point value
13AVERAGE=$(echo "$SUM/$TOTAL_INTEGERS" | bc -l)
14
15# print average rounded off to threbe decimal places
16printf "%.3f" $AVERAGE

This resulted in much simpler code and worked faster then the read example. (This also makes sense why the first line of the input contains the total number of integers to be added)