Mittwoch, 30. November 2011

Reading multiple return values with Bashs read function

It is quite easy to read a single return value of a sub process with Bash:

DATE=$(date)
echo $DATE
This command spawns a new process and stores the date and displays it. This is easy.

It becomes complicated if one needs the day and month in two different variables. Bashs standard function to parse input is read. So one could think this might be a possible way:

date | read DAY MONTH REST
echo $DAY $MONTH
But the above code prints only an empty line. The reason is that the pipe creates a new scope. An equivalent syntax for the above read is this:
date | (
    read DAY MONTH REST
)
echo $MONTH $DAY
Now it is obvious why the echo does not print anything. The variables are not valid any more when they get displayed, because the new scope has already been closed. One possible solution is to put the echo into the scope of the sub shell:
date | (
    read DAY MONTH REST
    echo $MONTH $DAY
)
Now the month and day is correctly displayed. But this solution has a major drawback. The standard input in the sub shell is the standard output of the date command and the original standard input of the surrounding shell gets shadowed and is not available any more in the sub shell.

The solution for this problem is: process substitution. The above command could be written this way using a process substitution:

(
    read DAY MONTH REST
    echo $MONTH $DAY
) < <(date)
The expression <(date) returns a file descriptor and the output of that file descriptor is redirected to the read block. You can try it with
echo <(date)
Which prints the file descriptor and
cat < <(date)
which prints the date. But this alone does not help much, because the read is still executed in a sub shell and the variables created by read are in a scope which does not have access to the original standard input. But by using a process substitutions it is possible to avoid the complete sub shell by redirecting the output of date directly into the read.

Normally read reads only from standard input but it is possible to specify a file descriptor by the use of the -u option. Unfortunately Bash uses a different syntax for the file descriptor. The process substitution prefixes the actual file descriptor number with /dev/fd/ while read expects the plain number. But the prefix can be stripped with Bashs parameter expansion functions. The ## operator matches the longest prefix and removes it:

FD=<(date)
echo $FD
echo ${FD##*/}

Combining all this in one line makes it possible to read more than one return value without introducing a new scope and without loosing the current standard input:

FD=<(date) read -u ${FD##*/} DAY MONTH REST
echo $MONTH $DAY