When working with command-line tools in Bash, we often encounter situations where we need to:
- View the full command output in real-time
- Simultaneously process that output (e.g., grep for specific patterns)
- Store the processed result in a variable for later use
The OP's initial attempts didn't work because:
mycommand | tee myvar=$(grep -c keyword) # Incorrect
mycommand | tee >(myvar=$(grep -c keyword)) # Subshell scope issue
Bash variable assignment in pipelines creates subshells where variable changes don't persist to the parent shell.
Method 1: Process Substitution with Temporary File
myvar=$(mycommand | tee >(grep -c keyword > /tmp/tmpfile) >/dev/null)
myvar=$(cat /tmp/tmpfile)
rm /tmp/tmpfile
Method 2: Named Pipe (FIFO)
mkfifo mypipe
mycommand | tee mypipe | grep -c keyword > myvar &
cat mypipe
wait
echo "Matches: $myvar"
Method 3: Bash Process Substitution with File Descriptor
exec 3>&1
myvar=$(mycommand | tee >(grep -c keyword >&3) | grep -c keyword)
exec 3>&-
Method 4: Using pee from moreutils
myvar=$(mycommand | pee 'grep -c keyword' 'cat')
For most use cases, the cleanest solution is:
# Store output in variable while displaying it
output=$(mycommand | tee /dev/tty)
myvar=$(grep -c keyword <<< "$output")
This approach:
- Preserves all output to terminal
- Captures full output in a variable
- Allows multiple processing steps
- No temporary files needed
Here's how you might monitor application logs while counting errors:
log_errors=$(tail -f /var/log/app.log | tee >(grep -c "ERROR" > error_count.txt) >/dev/null)
# In another terminal:
cat error_count.txt
For high-volume streams:
- Method 2 (FIFO) has lowest overhead
- Avoid multiple greps on large outputs
- Consider buffering (e.g.,
stdbuf -oL
) for interactive programs
stdbuf -oL mycommand | tee >(grep -c keyword > output) | cat
In zsh, you can use =(...)
process substitution:
myvar=$(grep -c keyword =(mycommand | tee /dev/tty))
If your variable remains empty:
set -x # Enable debugging
# Your command here
set +x # Disable debugging
When working with Linux command-line pipelines, we often need to both see the full output on screen while simultaneously processing parts of that output through other commands like grep and storing results in variables. The tee command seems perfect for this, but the syntax can be tricky to get right.
The failed approaches in the question reveal common misunderstandings:
# Problem 1: Command substitution happens first
mycommand | tee myvar=$(grep -c keyword) # grep runs before mycommand
# Problem 2: Process substitution scope
mycommand | tee >(myvar=$(grep -c keyword)) # Variable assignment happens in subshell
Solution 1: Using Process Substitution with Global Variable
This preserves the variable in the current shell:
myvar=$(mycommand | tee /dev/tty | grep -c keyword)
Solution 2: Multi-stage Processing with tee
For more complex processing:
{
mycommand | tee >(grep keyword > matches.txt) \
>(grep -v keyword > non_matches.txt) \
| grep -c keyword > count.txt
myvar=$(
Solution 3: Named Pipe Alternative
For persistent monitoring scenarios:
mkfifo mypipe
mycommand | tee mypipe &
myvar=$(grep -c keyword < mypipe)
When working with colorized output:
myvar=$(mycommand | tee >(cat >&2) | grep -c --color=always keyword)
For high-volume streams, these methods have different characteristics:
- Process substitution adds minimal overhead
- Named pipes have slightly more overhead but better for continuous streams
- Variable assignment is fastest for small outputs
Always include error handling:
if ! myvar=$(mycommand 2>&1 | tee /dev/tty | grep -c keyword); then
echo "Error occurred" >&2
exit 1
fi