Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash read function returns error code when using new line delimiter

I have a script that I am returning multiple values from, each on a new line. To capture those values as bash variables I am using the read builtin (as recommended here).

The problem is that when I use the new line character as the delimiter for read, I seem to always get a non-zero exit code. This is playing havoc with the rest of my scripts, which check the result of the operation.

Here is a cut-down version of what I am doing:

$ read -d '\n' a b c < <(echo -e "1\n2\n3"); echo $?; echo $a $b $c
1
1 2 3

Notice the exit status of 1.

I don't want to rewrite my script (the echo command above) to use a different delimiter (as it makes sense to use new lines in other places of the code).

How do I get read to play nice and return a zero exit status when it successfully reads 3 values?

Update

Hmmm, it seems that I may be using the "delimiter" wrongly. From the man page:

-d *delim* 

  The first character of delim is used to terminate the input line,
  rather than newline.

Therefore, one way I could achieve the desired result is to do this:

read -d '#' a b c < <(echo -e "1\n2\n3\n## END ##"); echo $?; echo $a $b $c

Perhaps there's a nicer way though?

like image 257
Lee Netherton Avatar asked Oct 22 '15 13:10

Lee Netherton


2 Answers

The "problem" here is that read returns non-zero when it reaches EOF which happens when the delimiter isn't at the end of the input.

So adding a newline to the end of your input will make it work the way you expect (and fix the argument to -d as indicated in gniourf_gniourf's comment).

What's happening in your example is that read is scanning for \ and hitting EOF before finding it. Then the input line is being split on \n (because of IFS) and assigned to $a, $b and $c. Then read is returning non-zero.

Using -d for this is fine but \n is the default delimiter so you aren't changing anything if you do that and if you had gotten the delimiter correct (-d $'\n') in the first place you would have seen your example not work at all (though it would have returned 0 from read). (See http://ideone.com/MWvgu7)

A common idiom when using read (mostly with non-standard values for -d is to test for read's return value and whether the variable assigned to has a value. read -d '' line || [ "$line" ] for example. Which works even when read fails on the last "line" of input because of a missing terminator at the end.

So to get your example working you want to either use multiple read calls the way chepner indicated or (if you really want a single call) then you want (See http://ideone.com/xTL8Yn):

IFS=$'\n' read -d '' a b c < <(printf '1 1\n2 2\n3 3')
echo $?
printf '[%s]\n' "$a" "$b" "$c"

And adding \0 to the end of the input stream (e.g. printf '1 1\n2 2\n3 3\0') or putting || [ "$a" ] at the end will avoid the failure return from the read call.

The setting of IFS for read is to prevent the shell from word-splitting on spaces and breaking up my input incorrectly. -d '' is read on \0.

like image 122
Etan Reisner Avatar answered Sep 28 '22 16:09

Etan Reisner


-d is the wrong thing to use here. What you really want is three separate calls to read:

{ read a; read b; read c; } < <(echo $'1\n2\n3\n')

Be sure that the input ends with a newline so that the final read has an exit status of 0.

If you don't know how many lines are in the input ahead of time, you need to read the values into an array. In bash 4, that takes just a single call to readarray:

readarray -t arr < <(echo $'1\n2\n3\n')

Prior to bash 4, you need to use a loop:

while read value; do
    arr+=("$value")
done < <(echo $'1\n2\n3\n')

read always reads a single line of input; the -d option changes read's idea of what terminates a line. An example:

$ while read -d'#' value; do
>    echo "$value"
> done << EOF
> a#b#c#
> EOF
a
b
c
like image 35
chepner Avatar answered Sep 28 '22 15:09

chepner