/dev/stdin with herestring

Steven Penny picture Steven Penny · Mar 11, 2013 · Viewed 9.9k times · Source

I would like a Bash script that can take input from a file or stdin, much like grep, for example

$ cat hw.txt
Hello world

$ grep wor hw.txt
Hello world

$ echo 'Hello world' | grep wor
Hello world

$ grep wor <<< 'Hello world'
Hello world

all works beautifully. However with the following script

read b < "${1-/dev/stdin}"
echo $b

It fails if using a herestring

$ hw.sh hw.txt
Hello world

$ echo 'Hello world' | hw.sh
Hello world

$ hw.sh <<< 'Hello world'
/opt/a/hw.sh: line 1: /dev/stdin: No such file or directory

Answer

Austin Phillips picture Austin Phillips · Mar 11, 2013

Using /dev/stdin in this manner can be problematic because you are attempting to get a handle to stdin using a name in the filesystem (/dev/stdin) rather than using the file descriptor which bash has already handed you as stdin (file descriptor 0).

Here's a small script for you to test:

#!/bin/bash

echo "INFO: Listing of /dev"
ls -al /dev/stdin

echo "INFO: Listing of /proc/self/fd"
ls -al /proc/self/fd

echo "INFO: Contents of /tmp/sh-thd*"
cat /tmp/sh-thd*

read b < "${1-/dev/stdin}"
echo "b: $b"

On my cygwin installation this produces the following:

./s <<< 'Hello world'


$ ./s <<< 'Hello world'
INFO: Listing of /dev
lrwxrwxrwx 1 austin None 15 Jan 23  2012 /dev/stdin -> /proc/self/fd/0
INFO: Listing of /proc/self/fd
total 0
dr-xr-xr-x 2 austin None 0 Mar 11 14:27 .
dr-xr-xr-x 3 austin None 0 Mar 11 14:27 ..
lrwxrwxrwx 1 austin None 0 Mar 11 14:27 0 -> /tmp/sh-thd-1362969584
lrwxrwxrwx 1 austin None 0 Mar 11 14:27 1 -> /dev/tty0
lrwxrwxrwx 1 austin None 0 Mar 11 14:27 2 -> /dev/tty0
lrwxrwxrwx 1 austin None 0 Mar 11 14:27 3 -> /proc/5736/fd
INFO: Contents of /tmp/sh-thd*
cat: /tmp/sh-thd*: No such file or directory
./s: line 12: /dev/stdin: No such file or directory
b: 

What this output shows is that bash is creating a temporary file to hold your HERE document (/tmp/sh-thd-1362969584) and making it available on file descriptor 0, stdin. However, the temporary file has already been unlinked from the file system and so is not accessible by reference through a file system name such as /dev/stdin. You can get the contents by reading file descriptor 0, but not by trying to open /dev/stdin.

On Linux, the ./s script above gives the following, showing that the file has been unlinked:

INFO: Listing of /dev
lrwxrwxrwx 1 root root 15 Mar 11 09:26 /dev/stdin -> /proc/self/fd/0
INFO: Listing of /proc/self/fd
total 0
dr-x------ 2 austin austin  0 Mar 11 14:30 .
dr-xr-xr-x 7 austin austin  0 Mar 11 14:30 ..
lr-x------ 1 austin austin 64 Mar 11 14:30 0 -> /tmp/sh-thd-1362965400 (deleted) <---- /dev/stdin not found
lrwx------ 1 austin austin 64 Mar 11 14:30 1 -> /dev/pts/12
lrwx------ 1 austin austin 64 Mar 11 14:30 2 -> /dev/pts/12
lr-x------ 1 austin austin 64 Mar 11 14:30 3 -> /proc/10659/fd
INFO: Contents of /tmp/sh-thd*
cat: /tmp/sh-thd*: No such file or directory
b: Hello world

Change your script to use the stdin supplied, rather than trying to reference through /dev/stdin.

if [ -n "$1" ]; then
    read b < "$1"
else
    read b
fi