5

Consider the following sample script:

#!/bin/sh

do_something() {
    echo $@
    return 1
}

cat <<EOF > sample.text
This is a sample text
It serves no other purpose
EOF

cat sample.text | while read arg1 arg2 arg3 arg4 arg5; do
    ret=0
    do_something "$arg1" "$sarg2" "$arg3" "$arg4" "$arg5" <&3 || ret=$?
done 3<&1

What is the purpose of redirecting stdout as input for filedescriptor 3? At least in Bash, it does not seem to make any difference if omitted. Does it have any effect if it is executed in any other shell than bash?

UPDATE

For those wondering where this is from, it is a simplified sample from Debian's cryptdisks_start script.

cxw
  • 16,685
  • 2
  • 45
  • 81
  • 1
    Where's this script from? I'm curious what the author intended. Since `do_something()` doesn't use its standard input, `<&3 == 0<&3` doesn't make any difference, as you observed. – cxw Jan 14 '17 at 14:38
  • Your usage of file descriptors has no impact on the overall logic you are going to do! – Inian Jan 14 '17 at 14:53
  • There's a tricky deadlock I don't quite understand yet if `do_something` *does* try to read from it standard input. – chepner Jan 14 '17 at 15:28
  • Basically, nothing has been written to the loop's standard output yet when `do_something` is called, so there is nothing on its standard input to read. – chepner Jan 14 '17 at 15:43
  • @cxw See my update –  Jan 14 '17 at 16:46
  • Frankly, this just looks buggy. Buggy code making it into real-world Linux distributions is... pretty much ubiquitous. – Charles Duffy Jan 14 '17 at 16:48
  • The author was probably trying to crib off some code that was using FD 3 to keep FD 0 open for stdin / TTY interaction, and got it wrong. – Charles Duffy Jan 14 '17 at 16:48
  • (Also, the use of `read arg1 arg2 ...` instead of `read -a args` and then `"${args[@]}"` later says something informative, and noncomplimentary, about code quality) – Charles Duffy Jan 14 '17 at 16:49
  • @chepner The original is from `cryptdisks_start` (See update). I hope I did not oversimplify the problem. –  Jan 14 '17 at 16:49
  • ...the useless use of `cat`, rather than putting ` – Charles Duffy Jan 14 '17 at 16:50
  • @CharlesDuffy This is a **sample** script, i.e., a **simplified version** of another script which I linked to in the **UPDATE** section of my post. –  Jan 14 '17 at 16:52
  • @nautical, ...so. It's **very** legit to prevent `do_something` from reading from your `sample.txt` stream, and that's a thing that it makes perfect sense for original code to be trying to do. – Charles Duffy Jan 14 '17 at 16:54
  • @nautical, ...if you want people to read the original code, **link to the actual code**, not a page describing the package it's taken from. – Charles Duffy Jan 14 '17 at 16:54
  • @CharlesDuffy There are download links for the source at the bottom of the page. –  Jan 22 '17 at 20:42
  • 1
    @nautical, in the days of web-based SCM interfaces (sourceforge, github, launchpad, etc), most projects have somewhere an individual line of source can be linked to directly on the web -- in light of which, "download this tarball, unpack it, and find the file containing [...]" isn't very reasonable a request. – Charles Duffy Jan 23 '17 at 14:50
  • @CharlesDuffy Well, I guess this something we disagree upon. I think it is a lot more convenient to have the files locally available then browsing them online, especially if the problem includes several files. The called function is originally sourced from another file (which further references a few other files). You can also use tools like `grep`, `find` etc. to further analyze the problem, which would be inconvenient to do online. Finally, AFAIK, `cryptdisks_start` is specific to Debian. So I thought it is best to link to the original source. –  Jan 24 '17 at 23:05

1 Answers1

12

The clear intent here is to prevent do_something from reading from the sample.text stream, by ensuring that its stdin is coming from elsewhere. If you're not seeing differences in behavior with or without the redirection, that's because do_something isn't actually reading from stdin in your tests.

If you had both read and do_something reading from the same stream, then any content consumed by do_something wouldn't be available to a subsequent instance of read -- and, of course, you'd have illegitimate contents fed on input to do_something, resulting in consequences such as a bad encryption key being attempted (if the real-world use case were something like cryptmount), &c.

cat sample.text | while read arg1 arg2 arg3 arg4 arg5; do
    ret=0
    do_something "$arg1" "$sarg2" "$arg3" "$arg4" "$arg5" <&3 || ret=$?
done 3<&1

Now, it's buggy -- 3<&1 is bad practice compared to 3<&0, inasmuch as it assumes without foundation that stdout is something that can also be used as input -- but it does succeed in that goal.


By the way, I would write this more as follows:

exec 3</dev/tty || exec 3<&0     ## make FD 3 point to the TTY or stdin (as fallback)

while read -a args; do           ## |- loop over lines read from FD 0
  do_something "${args[@]}" <&3  ## |- run do_something with its stdin copied from FD 3
done <sample.text                ## \-> ...while the loop is run with sample.txt on FD 0

exec 3<&-                        ## close FD 3 when done.

It's a little more verbose, needing to explicitly close FD 3, but it means that our code is no longer broken if we're run with stdout attached to the write-only side of a FIFO (or any other write-only interface) rather than directly to a TTY.


As for the bug that this practice prevents, it's a very common one. See for example the following StackOverflow questions regarding it:

etc.

Community
  • 1
  • 1
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Sorry for the late reply. I finally had some time to circle back to this problem. I was aware that `read` can yield "funny" if `stdin` is not properly redirected/protected. What had me stumped was the redirection of `stdout` as input. It seemed like the author was trying to enforce some pipe like behavior. I had never seen this before. Is there actually ever a valid reason to redirect `stdout` as done in my question? –  Jan 22 '17 at 20:40
  • @nautical, "ever" covers a lot of cases. Maybe stdin is opened to `/dev/null`, stdout is open read/write (so you *can* also read from it), and you don't have a controlling TTY. But that's a corner case, and someone has to *know* that their script is going to be used in that corner case for it to make sense. – Charles Duffy Jan 23 '17 at 14:49
  • 1
    @CharlesDuffy: I had trouble understanding the solution earlier before you added the code comments. The comments help me understand it now. Thanks. – adfaklsdjf Mar 15 '17 at 20:46