Friday, April 3, 2009

Tip: Redirecting Multiple Command Outputs

Let's imagine a simple script:
#!/bin/bash

echo 1
echo 2
echo 3
Simple enough. It produces three lines of output:
1
2
3
Now let's say we wanted to redirect the output of the commands to a file named foo.txt. We could change the script as follows:
#!/bin/bash

F=foo.txt

echo 1 >> $F
echo 2 >> $F
echo 3 >> $F
Again, pretty straightforward, but what if we wanted to pipe the output of all three echo commands into less? We would soon discover that this won't work:
#!/bin/bash

F=foo.txt

echo 1 | less
echo 2 | less
echo 3 | less
This causes less to be executed three times. Not what we want. We want a single instance of less to input the results of all three echo commands. There are four approaches to this:

Make A Separate Script
script1:
#!/bin/bash

echo 1
echo 2
echo 3
script2:
#!/bin/bash

script1 | less
By running script2, script1 is also executed and its output is piped into less. This works but it's a little clumsy.

Write A Shell Function
We could take the basic idea of the separate script and incorporate it into a single script by making script1 into a shell function:
#!/bin/bash

# shell function
run_echoes () {
echo 1
echo 2
echo 3
}

# call shell function and redirect
run_echoes | less
This works too, but it's not the simplest way to do it.

Make A List
We could construct a compound command using {} characters to enclose a list of commands:
#!/bin/bash

{ echo 1; echo 2; echo 3; } | less
The {} characters allow us to group the three commands into a single output stream. Note that the spaces between the {} and the commands, as well as the trailing semicolon after the third echo, are required.

Launch A Subshell
Finally, we could do this:
#!/bin/bash

(echo 1; echo 2; echo 3) | less
Placing the list inside () creates a subshell, or another copy of bash and it executes the commands. This has the same result as enclosing the list of commands within {} but with more overhead. The real reason we would want to do this is if, instead of just redirecting the output, we wanted to put all three commands in the background:
#!/bin/bash

(echo 1; echo 2; echo 3) > foo.txt &
This doesn't make much sense for our echo commands (they execute too quickly to bother with), but if we have commands that take a long time to run, this technique can come in handy.

Enjoy!