2

I'm trying to figure out how redirects could be used to selectively output a string from a bash function which needs to print multiple lines to the terminal. I found an answer on this question at StackOverflow. The code below works. But I don't understand how or why.

#!/bin/bash

exec 3>&1

returnString()
{
    exec 4>&1 >&3
    local s=$1
    s=${s:="some default string"}
    echo "writing to stdout"
    echo "writing to stderr" >&2
    exec >&4-
    echo "$s"
}

my_string=$(returnString "$*")
echo "my_string:  [$my_string]"

I understand that fd3 is redirected to stdout at the beginning. Then fd4 is as well, and 1 is pointed at 3, which basically points it back at itself. But how does this effect what is printed to the terminal, and why doesn't that output show up in $my_string, but the last one after getting rid of fd4 does?

I don't understand what fd4 has to do with anything, since nothing is explicitly sent to fd4.

3
  • 1
    You need to understand about shell pipes/redirection beyond stdin, stdout and stderr. Here is my favorite tutorial, if you wanna call it that. http://wiki.bash-hackers.org/howto/redirection_tutorial
    – MelBurslan
    Mar 18, 2016 at 18:47
  • Wow! That was helpful, but also a bit overwhelming. It seems to assume some understanding of how pipes are set up already though, which I don't have.
    – BryKKan
    Mar 18, 2016 at 19:38
  • dealing with file descriptors is no small thing to balk at. I never use them, never needed to. Good way to shoot yourself at foot in my opinion. I stick to K.I.S.S. principle : Keep It Stupidly Simple.
    – MelBurslan
    Mar 18, 2016 at 19:49

2 Answers 2

3

The solution you are showing above is one that is general purpose and covers a lot of cases. Unless you know you have an issue, you may want to adopt a YAGNI (You Ain't Gonna Need It) posture and just redirect your selected lines to /dev/tty.

If you want to avoid that, then you need to understand the file descriptor (fd) juggling that is going on.

The 3>&1 is copying the calling context's stdout to a "holding cell" so that the function can output to whatever the calling context's stdout is. In this case, it is /dev/tty. Remember, the function will be called with stdout belonging to a subshell for the ${} substitution.

The exec 4>&1 creates a copy of fd1, which is the stdout of the function. fd4 is being used as a holding cell for the stdout the function starts with. The >&3 is setting the function/subshell's stdout to the caller's stdout. Thus, all output will go to the caller's stdoout. UNTIL, the end where the >&4- moves the saved stdout back to where it started, allowing the last echo "$s" to be in the function(and subshell)'s stdout.

Whew!

The >&3 and >&4- could also be written more clearly as 1>&3 and 1>&4-.

The bash manual section on REDIRECTION, explains all the gory & nomenclature. I was pleased and surprised to see that it is actually possible to use {name} forms to change the numbers to words.

"Each redirection that may be preceded by a file descriptor number may instead be preceded by a word of the form {varname}. In this case, for each redirection operator except >&- and <&-, the shell will allocate a file descriptor greater than 10 and assign it to varname. If >&- or <&- is preceded by {varname}, the value of varname defines the file descriptor to close."

I didn't test this, but it suggests that you could code this as:

#!/bin/bash

exec {caller_stdio}>&1

returnString() {
  exec {func_stdio}>&1 1>&{caller_stdio}
  local s=$1
  s=${s:="some default string"}
  echo "writing to stdout"
  echo "writing to stderr" >&2
  exec 1>&{func_stdio}-
  echo "$s"
}

my_string=$(returnString "$*")
echo "my_string:  [$my_string]"
4
  • Very helpful explanation, and very interesting bit about the names. Could that possibly collide with another named variable though?
    – BryKKan
    Mar 18, 2016 at 20:03
  • 1
    My guess is that it bash probably uses the same symbol table for all its symbols. So you would need to choose your names to avoid collision just as you would in a shell script.
    – Greg Tarsa
    Mar 18, 2016 at 21:56
  • 1
    I cannot comment on StackExchange questions (yet), but to your question in the other answer about what the exec >&4- does, is that it moves the fd from 4 to 1. See the "Moving File Descriptors" subsection in the REDIRECTION section of the bash manual for full details. That entire section would be worth a read if you want to get a good handle on this feature. It is very terse, but quite clear.
    – Greg Tarsa
    Mar 18, 2016 at 21:59
  • Thank you for the suggestion. I've been reading this manual. It's making much more sense after reading these answers.
    – BryKKan
    Mar 18, 2016 at 22:35
3
exec 3>&1

fd 3 is now a dup of fd 1 (in your example, your terminal). As the man page says, it means that fd 1 and fd 3 can be used interchangeably to refer to the same file or device.

The major use of duping in the shell is to save a copy of an fd so that it can be restored later.

my_string=$(returnString "$*")

In order for bash to get the stdout output from the evaluation of returnString "$*", it creates a pipe and forks, and the child calls dup2 to move the write end of the pipe (in this example, it's fd 5, but that can vary) to fd 1.

exec 4>&1 >&3

Redirections are evaluated left to right. fd 4 is set to be a dup of the pipe end. Then, fd 1 is set to be a dup of the fd that refers to your terminal.

echo "writing to stdout"

echoes to fd 1, your terminal.

exec >&4-

This moves (that is, does a dup2) of fd 4 to fd 1. fd 1 is now the pipe end.

echo "$s"

echoes to the pipe end. Eventually, this is read by the parent shell from the other end of the pipe and used as the result of $(returnString "$*")

6
  • Very helpful! Can you clarify this statement though: In order for bash to get the stdout output from the evaluation of returnString "$*", it creates a pipe and forks, and the child calls dup2 to move the write end of the pipe (in this example, it's fd 5, but that can vary) to fd 1. How do you know it would be fd5 in this case?
    – BryKKan
    Mar 18, 2016 at 19:52
  • The open, dup, and pipe system calls used by the shell always use the lowest available fd. Your script started with fd's 0, 1, and 2 open (as usual). then exec 3 allocated fd 3, then the pipe allocated 4 and 5, with 5 subsequently closed by the parent and moved to 1 by the child just before returnString was called. I saw all this by using strace -f on your shell script. Mar 18, 2016 at 20:07
  • Thank you, also very helpful! I'm trying to apply this to a larger script and running into problems, so being able to watch what is happening with the fd's would be helpful.
    – BryKKan
    Mar 18, 2016 at 20:10
  • in the step where fd 4 is duped to fd 1 (exec >&4-), any idea why the trailing - exists? I was thinking exec >&4 should suffice
    – iruvar
    Mar 18, 2016 at 21:32
  • Because we don't need fd4 anymore so we're cleaning up by closing it?
    – BryKKan
    Mar 18, 2016 at 21:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .