How to save /dev/stdout target location in a bash script?

To save a file descriptor, you duplicate it on another fd. Saving a path to the corresponding file is not enough, you'd need to save the opening mode, the opening flags, the current position within the file and so on. And of course, for anonymous pipes, or sockets, that wouldn't work as those have no path. What you want to save is the open file description that the fd refers to, and duplicating an fd is actually returning a new fd to the same open file description.

To duplicate a file descriptor onto another, with Bourne-like shell, the syntax is:

exec 3>&1

Above, fd 1 is duplicated onto fd 3.

Whatever fd 3 was already open to before would be closed, but note that fds 3 to 9 (usually more, up to 99 with yash) are reserved for that purpose (and have no special meaning contrary to 0, 1, or 2), the shell knows not to use them for its own internal business. The only reason fd 3 would have been open beforehand is because you did it in the script1, or it was leaked by the caller.

Then, you can change stdout to something else:

exec > /dev/null

And later, to restore stdout:

exec >&3 3>&-

(3>&- being to close the file descriptor which we no longer need).

Now, the problem with that is that except in ksh, every command you run after that exec 3>&1 will inherit that fd 3. That's a fd leak. Generally not a big deal, but that can cause problem.

ksh sets the close-on-exec flag on those fds (for fds over 2), but not other shells and other shells don't have any way to set that flag manually.

The work around for other shell is to close the fd 3 for each and every command, like:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Cumbersome. Here, the best way would be to not use exec at all, but redirect command groups:

{
  ls
  uname
} > file.log

There, it's the shell that takes care to save stdout and restore it afterwards (and it does do it internally by duplicating it on a fd (above 9, above 99 for yash) with the close-on-exec flag set).

Note 1

Now, the management of those fds 3 to 9 can be cumbersome and problematic if you use them extensively or in functions, especially if your script uses some third party code that may in turn use those fds.

Some shells (zsh, bash, ksh93, all added the feature (suggested by Oliver Kiddle of zsh) around the same time in 2005 after it was discussed among their developers) have an alternative syntax to assign the first free fd above 10 instead which helps in this case:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}

As you can see, bash scripting is not like a regular programming language where you can assign file descriptors.

The simplest solution is to use a sub-shell to run what you want redirected so that processing can be reverted to the top-shell which has its standard I/O intact.

An alternate solution would be to use tty to identify the TTY device and control the I/O in your script. For example:

dev=$(tty)

and then you can..

echo message > $dev

$$ would get you the current process PID, in case of interactive shell or script the relevant shell PID.

So you can use:

readlink -f /proc/$$/fd/1

Example:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33