Bash – read -a array -d ‘\n’ < foo, exit code 1

bashnewlinesread

If I try to execute

read -a fooArr -d '\n' < bar

the exit code is 1 — even though it accomplishes what I want it to; put each line of bar in an element of the array fooArr (using bash 4.2.37).

Can someone explain why this is happening


I've found other ways to solve this, like the ones below, so that's not what I'm asking for.

for ((i=1;; i++)); do
    read "fooArr$i" || break;
done < bar

or

mapfile -t fooArr < bar

Best Answer

What needs to be explained is that the command appeared to work, not its exit code

'\n' is two characters: a backslash \ and a letter n. What you thought you needed was $'\n', which is a linefeed (but that wouldn't be right either, see below).

The -d option does this:

  -d delim  continue until the first character of DELIM is read, rather
            than newline

So without that option, read would read up to a newline, split the line into words using the characters in $IFS as separators, and put the words into the array. If you specified -d $'\n', setting the line delimiter to a newline, it would do exactly the same thing. Setting -d '\n' means that it will read up to the first backslash (but, once again, see below), which is the first character in delim. Since there is no backslash in your file, the read will terminate at the end of file, and:

Exit Status:
The return code is zero, unless end-of-file is encountered, read times out,
or an invalid file descriptor is supplied as the argument to -u.

So that's why the exit code is 1.

From the fact that you believe that the command worked, we can conclude that there are no spaces in the file, so that read, after reading the entire file in the futile hope of finding a backslash, will split it by whitespace (the default value of $IFS), including newlines. So each line (or each word, if a line contains more than one word) gets stashed into the array.

The mysterious case of the purloined backslash

Now, how did I know the file didn't contain any backslashes? Because you didn't supply the -r flag to read:

  -r                do not allow backslashes to escape any characters

So if you had any backslashes in the file, they would have been stripped, unless you had two of them in a row. And, of course, there is the evidence that read had an exit code of 1, which demonstrates that it didn't find a backslash, so there weren't two of them in a row either.

Takeaways

Bash wouldn't be bash if there weren't gotchas hiding behind just about every command, and read is no exception. Here are a couple:

  1. Unless you specify -r, read will interpret backslash escape sequences. Unless that's actually what you want (which it occasionally is, but only occasionally), you should remember to specify -r to avoid having characters disappear in the rare case that there are backslashes in the input.

  2. The fact that read returns an exit code of 1 does not mean that it failed. It may well have succeeded, except for finding the line terminator. So be careful with a loop like this: while read -r LINE; do something with LINE; done because it will fail to do something with the last line in the rare case that the last line doesn't have a newline at the end.

  3. read -r LINE preserves backslashes, but it doesn't preserve leading or trailing whitespace.

Related Question