8

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
1
  • 1
    In your case, by the way, it's easy to work around this by writing if [[ $# = 0 ]] ; then read b ; else read b < "$1" ; fi. But I have no idea why such a workaround should be necessary. Commented Mar 11, 2013 at 2:58

4 Answers 4

12

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
Sign up to request clarification or add additional context in comments.

6 Comments

This is actually not true in this specific case. Bash handles any of the /dev/fd/* internally if used either in a redirection or within a test expression (which is why they're listed in the manual). Provided you're using Bash or ksh93, those can be used portably so long as they aren't just supplied as arguments to a builtin or external command. You can even write to a Bash herestring using /dev/stdin. However, not all shells use temporary files for heredocs. Dash/busybox use pipes.
@ormaaj Interesting. The manual states that /dev/stdin is handled specially, but my initial reading of the bash source would indicate that if /dev/stdin was available during the configure phase, then /dev/stdin would be treated as any other file. ie in the source, HAVE_DEV_STDIN would be defined and so not appear in the special filenames list.
That could be, but it also shouldn't really matter. This works fine here on Linux: dash -c 'x=$(mktemp); echo test >"$x"; { unlink -- "$x"; cat; cat /proc/self/fd/0; } <"$x"'
@ormaaj While this works on Linux, it fails under Cygwin. If you run strace on the dash command you can see that /proc/self/fd/0 is opened and allocated its own file descriptor, rather than a dup call. I don't fully understand, but on Linux, it appears as though opening /proc/self/fd/0 -> foo (deleted file) is allowed and gives back the contents of the file.
On Linux, echo test > /tmp/foo; { rm /tmp/foo; echo A; cat /tmp/foo; echo B; cat /proc/self/fd/0; } < /tmp/foo, gives the output A cat: /tmp/foo: No such file or directory B test showing that /proc/self/fd/0 can be opened and produces the original text. On cygwin the output is A cat: /tmp/foo: No such file or directory B cat: /proc/self/fd/0: No such file or directory . Since /proc/ is only emulated on Cygwin and not part of the kernel, this is likely the reason for the difference.
|
1

bash parses some file names (like /dev/stdin) specially, so that they are recognized even if they are not actually present in the file system. If your script doesn't have #!/bin/bash at the top, and /dev/stdin isn't in your file system, your script may be run using /bin/sh, which would expect /dev/stdin to actually be a file.

(This should, perhaps, not be an answer, but rather a comment to Austin's answer.)

2 Comments

See my comment to ormaaj. It seems to me that /dev/stdin will only be treated specially if it doesn't exist at compile time. Would be interested to have someone else read the code and verify.
Interesting! I've only gone by what the man page says, and specifically have never really looked at the usage on machines with and without a real file-system entry.
0
$ cat ts.sh 
read b < "${1-/dev/stdin}"
echo $b

$ ./ts.sh <<< 'hello world'
hello world

No problem for me. I'm using bash 4.2.42 on Mac OS X.

Comments

-1

You got a typo here

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

Try

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

4 Comments

Both notations are acceptable; the shell handles both. Whether the version without the colon works as intended is more questionable.
${1-/dev/stdin} only replaces $1 with "/dev/stdin" if $1 is unset. ${1:-/dev/stdin} will also replace $1 if it is set to the empty string.
Interesting, can't find any reference to ${parameter-word} expansion in bash man pages. But it works like chepner said... My bad.
The documentation is vague; there is a single sentence in the man page just prior to the list of the various operators that describes the effect of omitting the colon from any of them. It's hard to search for, and very easy to miss if you are just skimming.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.