4

I have a bash script that calls a function. The function, amongst other things, executes a pipeline that sinks its output. To simplify it, here is a contrived example:

#!/bin/bash
func() {
  ls "$@" | sort | rev > /tmp/output
}
func "$@"

You would then do something like this to run it. It would do its stuff and deliver its payload to a file. There is no output to the screen.

$ ./myscript .

Now, say I want the output of sort on standard output. How would I do that?

I can achieve it like this:

ls "$@" | sort | tee /dev/tty | rev > /tmp/output

However, using /dev/tty is wrong, because this won't work:

$ ./myscript > myfile

Is there a more correct way to refer to the standard output of a bash script from within a pipleine inside a function ?

(I am using bash 4.3.0 on Arch Linux)

starfry
  • 7,442
  • 1
    Is there any reason to specify an output file? Why not just print to stdout directly? ls "$@" | sort | rev. – terdon Oct 16 '14 at 14:01
  • That's just a contrived example. The real use would see the end of the pipe delivering to a service (a socket or remote host:port). – starfry Oct 16 '14 at 15:37
  • in most cases you can do tee /dev/fd/[num] to tee your output to anywhere. echo 'out twice' | { { tee /dev/fd/2 | grep . >&3 ; } 2>&1 | grep . ; } 3>&1 | nl – mikeserv Oct 16 '14 at 16:30
  • I think the other question has a different context that would not obviously solicit the answers given to this question. I think you could argue it is similar, but hardly a duplicate. I'd like this to be re-opened so other interesting answers can be given. – starfry Oct 17 '14 at 08:48
  • @starfry I may be missing something but the answers of the duplicate are basically the same as the ones you have here. Namely, tee and creating a separate file descriptor. The other question is asking the same thing also, how to get the output of a part of a pipeline. I don't see the difference you refer to but that may just be my own ignorance. If you still feel that it's not a dupe, please post a question on [meta] where it can be discussed. – terdon Oct 17 '14 at 14:59
  • @starfry - I've looked at this as well, and we discussed it in the chatroom, it seems like a dup to several of us. – slm Oct 17 '14 at 15:01

2 Answers2

4

The simplest way I can think of doing this is:

ls "$@" | sort | tee >(rev > /tmp/output)

The tee will send one copy to STDOUT, and since there is no longer a | after it, this is inherited, meaning it'll go to the TTY if not redirected, and your myfile if it is.
The other copy will get sent to rev > /tmp/output on its STDIN. In bash, >(...) runs whatever is between the parenthesis and then substitutes the >(...) with the path to a pipe connected to its STDIN.

phemmer
  • 71,831
  • Yes, that certainly works. I know about process substitution but using it to solve this didn't come to mind. I thought there might be another way to get the file descriptor of the outer script's stdin but this method works. – starfry Oct 16 '14 at 15:45
  • Well, you can dup the file descriptor to a new one (exec {FD}>&1), but that doesn't solve the problem. You could use tee and redirect one copy to /proc/$$/fd/$FD, but that's linux & bash specific. The solution in the answer is only bash specific (it's portable). – phemmer Oct 16 '14 at 15:54
  • I know it isn't the way to go (your answer is), but what would FD be in that case? – starfry Oct 16 '14 at 15:58
  • I can't find it after a quick search through the bash man page, but exec {FD}>&1 creates a new file descriptor which is a duplicate of 1, and assigns the file descriptor number to $FD. – phemmer Oct 16 '14 at 16:05
  • Nevermind, I just didn't look hard enough. That is documented in the second paragraph of the REDIRECTION section. – phemmer Oct 16 '14 at 21:24
3

Since the other answer isn’t being clear about this, the other (another) way is

exec 3>&1
ls | sort | tee /dev/fd/3 | rev > /tmp/output

The exec 3>&1 duplicates file descriptor 1 (stdout) as file descriptor 3.  Then tee /dev/fd/3 writes a copy of sort’s output to that file descriptor.  This should work in any shell, but it may be dependent on the operating system.  A variant that should be OS-independent but is bash-specific is:

exec 3>&1
ls | sort | tee >( cat >&3 ) | rev > /tmp/output

In bash, >(command) gives you a file handle to a pipe to a process running command.  In any (?) shell, command >&fd runs command with its stdout redirected to the specified file descriptor, which must have been previously established.

A more complicated version, which should work on any *nix (Posix) system, is

pipe_to_stdout=$(mktemp -u)
mkfifo "$pipe_to_stdout"
cat "$pipe_to_stdout" &
ls | sort | tee "$pipe_to_stdout" | rev > /tmp/output
rm "$pipe_to_stdout"

which is pretty much the same as the second example (the cat reads from the fifo and writes to stdout), but using less smoke and mirrors at the shell level.

Note that these answers are flexible, in that you could write the output of the ls to the standard output just by rearranging the commands.