Why is set-o errexit breaking this read/heredoc expression?

read returns a non-zero exit status if it doesn't find a delimiter, which is always the case when the delimiter is an empty string.


The exit code of the read command is 1 when the end of file (EOF) marker is reached. This will always happen when the delimiter -d is null '' in this special case where the source stream is a heredoc that could not contain a \0.

$ read -d '' message <<-_ThisMessageEnds_
>     this is a
>     multi line
>     message
> _ThisMessageEnds_
$ exitval=$?
$ echo "The exit val was $exitval"
The exit val was 1.

That the exit value is an error (not 0) makes the script exit could be avoided with a AND/OR construct:

read -d '' message <<-_ThisMessageEnds_ || echo "$message"
    this is a
    multi line
    message
_ThisMessageEnds_

That will send the message to the console and yet avoid exiting it with errexit.

But as we are on this path to reduce, why not use this directly:

cat <<-_ThisMessageEnds_
    this is a
    mulitline
    message
_ThisMessageEnds_

No read command executed (more speed), no variable needed, no error from the exit code, less code to maintain.


read -d '' message

reads stdin until the first unescaped (as you didn't add -r) NUL character or the end of the input and stores the data after $IFS and backslash character processing into $message (without the delimiter).

If no unescaped delimiter is found in the input, read's exit status is non-zero. It only returns 0 (success) if a full, terminated record is read.

It's most useful for dealing with NUL-delimited records like the output of find -print0 (though you then need a IFS= read -rd '' record syntax).

Here, you need to include a NUL delimiter in your here-document for read to return successfully. That is however not possible with bash which strips NUL characters from here-documents (that's at least better than yash that strips everything past the first NUL, or ksh93 which seems to enter an infinite loop when a here-document contains a NUL).

zsh is the only shell that can have a NUL in its here documents or store it in its variables or pass NUL characters in arguments to its builtins/functions. In zsh, you can do:

NUL=$'\0'
IFS= read -d $NUL -r var << EOF
1
2
3$NUL
EOF

(zsh also understand read -d '' as a NUL delimiter like bash. read -d $'\0' also works in bash but that does pass an empty argument to read like in read -d '' as bash doesn't support NUL bytes in its command line).

(note that there's an extra newline character after that $NUL)

In bash, you can use a different character:

ONE=$'\1'
IFS= read -d "$ONE" -r var << EOF
1
2
3$ONE
EOF

But you could also do:

var=$(cat <<EOF
message
here
EOF
)

That will still not allow NUL characters. That's however standard code, so you don't need to rely on the zsh/bash specific read -d. Also note that it removes all trailing newline characters, and except in ksh93 when the cat builtin is enabled, that means spawning an extra process and command.

Tags:

Bash