How to use `while read` (Bash) to read the last line in a file if there’s no newline at the end of the file?

83,641

Solution 1

In your first example, I'm assuming you are reading from stdin. To do the same with the second code block, you just have to remove the redirection and echo $REPLY:

DONE=false
until $DONE ;do
read || DONE=true
echo $REPLY
done

Solution 2

I use the following construct:

while IFS= read -r LINE || [[ -n "$LINE" ]]; do
    echo "$LINE"
done

It works with pretty much anything except null characters in the input:

  • Files that start or end with blank lines
  • Lines that start or end with whitespace
  • Files that don't have a terminating newline

Solution 3

Using grep with while loop:

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)

Using grep . instead of grep "" will skip the empty lines.

Note:

  1. Using IFS= keeps any line indentation intact.

  2. You should almost always use the -r option with read.

  3. File without a newline at the end isn't a standard unix text file.

Solution 4

Instead of read, try to use GNU Coreutils like tee, cat, etc.

from stdin

readvalue=$(tee)
echo $readvalue

from file

readvalue=$(cat filename)
echo $readvalue

Solution 5

This is the pattern I've been using:

while read -r; do
  echo "${REPLY}"
done
[[ ${REPLY} ]] && echo "${REPLY}"

Which works because even tho' the while loop ends as the "test" from the read exits with a non-zero code, read still populates the inbuilt variable $REPLY (or whatever variables you choose to assign with read).

Share:
83,641
Mathias Bynens
Author by

Mathias Bynens

I work on Chrome and web standards at Google. ♥ JavaScript, HTML, CSS, HTTP, performance, security, Bash, Unicode, macOS.

Updated on July 20, 2022

Comments

  • Mathias Bynens
    Mathias Bynens 11 months

    Let’s say I have the following Bash script:

    while read SCRIPT_SOURCE_LINE; do
      echo "$SCRIPT_SOURCE_LINE"
    done
    

    I noticed that for files without a newline at the end, this will effectively skip the last line.

    I’ve searched around for a solution and found this:

    When read reaches end-of-file instead of end-of-line, it does read in the data and assign it to the variables, but it exits with a non-zero status. If your loop is constructed "while read ;do stuff ;done

    So instead of testing the read exit status directly, test a flag, and have the read command set that flag from within the loop body. That way regardless of reads exit status, the entire loop body runs, because read was just one of the list of commands in the loop like any other, not a deciding factor of if the loop will get run at all.

    DONE=false
    until $DONE ;do
    read || DONE=true
    # process $REPLY here
    done < /path/to/file.in
    

    How can I rewrite this solution to make it behave exactly the same as the while loop I was having earlier, i.e. without hardcoding the location of the input file?