9

I found some code for reading input from a file a while ago, I believe from Stack Exchange, that I was able to adapt for my needs:

while read -r line || [[ -n "$line" ]]; do
    if [[ $line != "" ]]
    then
        ((x++));
        echo "$x:  $line"
    <then do something with $line>
    fi
done < "$1"

I'm reviewing my script now & trying to understand what it's doing ... I don't understand what this statement is doing:

while read -r line || [[ -n "$line" ]];

I understand that the -r option says that we're reading raw text into line, but I'm confused about the || [[ -n "$line" ]] portion of the statement. Can someone please explain what that is doing?

2 Answers 2

9

[[ -n "$line" ]] tests if $line (the variable just read by read) is not empty. It's useful since read returns a success if and only if it sees a newline character before the end-of-file. If the input contains a line fragment without a newline in the end, this test will catch that, and the loop will process that final incomplete line, too. Without the extra test, such an incomplete line would be read into $line, but ignored by the loop.

I said "incomplete line", since the POSIX definitions of a text file and a line require a newline at the end of each line. Other tools than read can also care, e.g. wc -l counts the newline characters, and so ignores a final incomplete line. See e.g. What's the point in adding a new line to the end of a file? and Why should text files end with a newline? on SO.

The cmd1 || cmd2 construct is of course just like the equivalent in C. The second command runs if the first returns a falsy status, and the result is the exit status of the last command that executed.

Compare:

$ printf 'foo\nbar' | ( while read line; do echo "in loop: $line"; done;
                        echo "finally: $line" )
in loop: foo
finally: bar

and

$ printf 'foo\nbar' | ( while read line || [[ -n $line ]]; do 
                           echo "in loop: $line"; done; 
                        echo "finally: $line" )
in loop: foo
in loop: bar
finally: 
2
  • A missing LF at EOF totally slipped my mind. Good catch on that...
    – brhfl
    Oct 30, 2018 at 20:00
  • Thank you both for your answers ... that clears it up.
    – K. Hilbert
    Oct 30, 2018 at 20:10
1

It's a bit confusing as to why that would be there, but straightforward to explain what it does: || is an OR statement, and [[ -n returns true (success) as long as "$line" has a nonzero length. Here's what's confusing: while loops keep going while there's a successful (0) exit status. read continues to read lines and return a 0 exit status until it hits end of file -- even if those lines are blank. [[ -n "$line" ]] is only going to execute when read returns a nonzero exit code, at which point $line will be empty. Since the test returns true if $line is not empty, we're back to a nonzero exit, throwing us out of the while loop. As far as I can see, || [[ -n "$line" ]] doesn't actually accomplish anything. (As @ilkkachu pointed out, this will catch the odd final line of input that is missing its trailing newline. Note that such a file is not a valid text file, as that line is not a valid line)

Something that is occasionally useful is to do while read -r line && [[ -n "$line" ]]. Using && (AND) means that the whole statement will only return a zero status if read is able to read a line, AND that line is not empty. It will cause the while loop to halt at the first empty line. If I had to make a guess, this snippet of code may have been adapted from one which did just that - and instead of simply removing the test, the author changed the && to ||.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .