Home > Software engineering >  pipe of "ps -e", "tee" and "head" behaves strange
pipe of "ps -e", "tee" and "head" behaves strange

Time:05-22

I meant to use the following command to grep the "zsh" process with header. But this command only showed the header and then exited. The grep didn't run. I can do this task by other ways but I want to understand what happened here. The interesting thing is that the pipe works as expected if other command like ls replaces ps -e. In fact, the pipe works even with ps (without -e). The pipe also works if head is replaced by another grep. Seemingly, there are some interesting interactions between ps -e, head and tee in this pipe. Any explanation? I ran the commands in MacOS-iterm2 and zsh. Thanks.

 ps -e|tee >(head -n 1) >(grep "zsh") >/dev/null

the command above produces the following incorrect output: only header is shown, grep didn't run.

PID TTY           TIME CMD

The correct output can be generated by the following command:

ps -e|awk 'FNR==1{print};/zsh/{print}'
  PID TTY           TIME CMD
 1658 ttys000    0:00.30 -zsh
 2817 ttys001    0:00.49 /bin/zsh -i
12890 ttys002    0:00.26 -zsh
13332 ttys003    0:00.23 -zsh
13469 ttys004    0:00.19 /bin/zsh -i

Update: The problem is indeed in the tee shipped with MacOS. As @OndrejK. pointed out in the comments below, tee from GNU works as expected.

CodePudding user response:

I think Ondrej K. nailed it in a comment: head is reading just the first line it receives from tee, then exiting, so when tee tries to write the second line to that pipe, it gets an error (probably actually a SIGPIPE signal) and exits without writing the rest of ps's output.

I was able to reproduce this problem under both macOS and Raspbian, using both zsh and bash as my shell. I was able to get the full output by replacing head with something that reads (but doesn't output) the rest of the input from tee. Here's an example:

% ps -e|tee >(head -n 1) >(grep "zsh") >/dev/null
  PID TTY          TIME CMD
% ps -e|tee >(head -n 1; cat>/dev/null) >(grep "zsh") >/dev/null
  PID TTY          TIME CMD
13127 pts/0    00:00:00 zsh
13353 pts/0    00:00:00 zsh
% ps -e|tee >(sed -n '1p') >(grep "zsh") >/dev/null             
  PID TTY          TIME CMD
13127 pts/0    00:00:00 zsh

In the second version above, adding cat>/dev/null after head lets it consume (and discard) the rest of the input after head exits. In the third, sed -n '1p' reads its entire input, but only prints the first line.

Note that the result here will depend on things like buffer sizes and the timing of various events, so it may be inconsistent between different environments or even vary randomly under apparently consistent conditions. I suspect the reason it works with ps (without the -e option) is that it doesn't produce enough output to trigger the problem: tee may be able to forward the entire output from ps before head gets around to exiting.

EDIT: At least in my environments, adding a delay after head exits prevents the problem:

% ps -e|tee >(head -n 1; sleep 1) >(grep "zsh") >/dev/null      
  PID TTY          TIME CMD
 8002 pts/0    00:00:00 zsh
 8164 pts/0    00:00:00 zsh

This means it's timing-limited, rather than buffer-limited. But if the output from ps were big enough, it'd run into buffer limits at some point, and the output would be truncated no matter what delays were inserted.

  • Related