9

Let's say I have a bash script called log.sh. In this script, I want to read in input from a pipe, but I also want to know the command used to pipe input into me. Example:

tail -f /var/log/httpd/error | log.sh

In the shell script, I want to know the command tail -f /var/log/httpd/error.

5
  • I am extremely curious as to why you want this. Feb 11, 2011 at 6:41
  • My guess is your making some type of GUI program which captures & processes pids? Feb 11, 2011 at 6:50
  • I would like to know so I can distinguish where to put the results. Depending on the command and filename, I would want to perform different actions. Feb 11, 2011 at 6:57
  • 4
    That sounds like a major violation of the Principle of Least Surprise to me. If the script should do different things under different circumstances, then that should be controlled by a command-line option rather than by where its input is coming from. Feb 11, 2011 at 10:10
  • @Dave, I agree. Let's just say, for the sake of this example, I just want to 'know' what the incoming command is. Feb 11, 2011 at 16:57

3 Answers 3

7

Akira suggested using lsof.

Here's how you could script it:

whatpipe2.sh

#!/bin/bash

pid=$$
pgid=$(ps -o pgid= -p $pid)
lsofout=$(lsof -g $pgid)
pipenode=$(echo "$lsofout" | awk '$5 == "0r" { print $9 }')
otherpids=$(echo "$lsofout" | awk '$5 == "1w" { print $2 }')
for pid in $otherpids; do
    if cmd=$(ps -o cmd= -p $pid 2>/dev/null); then
        echo "$cmd"
        break
    fi
done

Running it:

$ tail -f /var/log/messages | ./whatpipe2.sh
tail -f /var/log/messages
^C

Another way is using process groups.

whatpipe1.sh

#!/bin/bash    

pid=$$
# ps output is nasty, can (and usually does) start with spaces
# to handle this, I don't quote the "if test $_pgrp = $pgrp" line below
pgrp=$(ps -o pgrp= -p $pid)
psout=$(ps -o pgrp= -o pid= -o cmd=)
echo "$psout" | while read _pgrp _pid _cmd; do
    if test $_pgrp = $pgrp; then
        if test $_pid != $pid; then
            case $_cmd in
            ps*)
                # don't print the "ps" we ran to get this info
                # XXX but this actually means we exclude any "ps" command :-(
                ;;
            *)
                echo "$_cmd"
                ;;
            esac
        fi
    fi
done

Running it:

$ tail -f /var/log/messages | ./whatpipe1.sh
tail -f /var/log/messages
^C

Note they both only work if the command on the left side of the pipe runs for long enough for ps to see it. You said you were using it with tail -f, so I doubt this is an issue.

$ sleep 0 | ./whatpipe1.sh 

$ sleep 1 | ./whatpipe1.sh
sleep 1
3
  • instead of this huge post i would have given a 2nd answer with the lsof-based script. nice work for that one.
    – akira
    Feb 11, 2011 at 10:02
  • @akira Thanks. It took a few attempts to make it clean and portable. Learned a few things about procfs and lsof along the way. Thanks for the idea.
    – Mikel
    Feb 11, 2011 at 10:04
  • I accepted yours as it gives an answer other people can directly use. @Akira, you did most of the work, sorry I couldn't accept yours as well. Feb 11, 2011 at 17:02
10

the pipe will appear as an entry in the list of open filedescriptors of your process:

 % ls -l /proc/PID/fd
 lr-x------ 1 xyz xyz 64 Feb 11 08:05 0 -> pipe:[124149866]
 lrwx------ 1 xyz xyz 64 Feb 11 08:05 1 -> /dev/pts/2
 lrwx------ 1 xyz xyz 64 Feb 11 08:05 2 -> /dev/pts/2
 lr-x------ 1 xyz xyz 64 Feb 11 08:05 10 -> /tmp/foo.sh

you could also use something like:

 % lsof -p PID
 sh      29890 xyz  cwd    DIR   0,44    4096  77712070 /tmp
 sh      29890 xyz  rtd    DIR   0,44    4096  74368803 /
 sh      29890 xyz  txt    REG   0,44   83888  77597729 /bin/dash
 sh      29890 xyz  mem    REG   0,44 1405508  79888619 /lib/tls/i686/cmov/libc-2.11.1.so
 sh      29890 xyz  mem    REG   0,44  113964  79874782 /lib/ld-2.11.1.so
 sh      29890 xyz    0r  FIFO    0,6         124149866 pipe
 sh      29890 xyz    1u   CHR  136,2                 4 /dev/pts/2
 sh      29890 xyz    2u   CHR  136,2                 4 /dev/pts/2
 sh      29890 xyz   10r   REG   0,44      66  77712115 /tmp/foo.sh

so, than you have the inode of the pipe :) you can now search every other process under /proc/ for that pipe. then you will have the command that is piping to you:

 % lsof | grep 124149866 
 cat     29889 xyz    1w  FIFO                0,6          124149866 pipe
 sh      29890 xyz    0r  FIFO                0,6          124149866 pipe

in this example, cat piped to wards sh. in /proc/29889 you can find a file called cmdline which tells you, what exactly was called:

 % cat /proc/29889/cmdline
 cat/dev/zero%  

the fields of the command line are separated by NUL, thus it looks a bit ugly :)

1
  • I don't know which answer to accept. @Akira, you gave the actual breakdown as to how to determine it, while @Mikel gave me a script. Way to make things awesome + difficult. Feb 11, 2011 at 16:59
1

Here's a compact solution using modern lsof on modern Linux distributions:

cmd=$(lsof -t -p $$ -a -d 0 +E | while read p; do
    [ $p -ne $$ ] && echo "$(tr \\000 " " </proc/$p/cmdline)"
done)

This lists the endpoint files (+E) of FD 0 on the current shell process (-p $$ -a -d 0), then limits the output to PIDs only (-t), yielding the PIDs on both sides of the pipe.

Note that:

  1. There may be more than one PID found on the source end, e.g. { echo Hi; sleep 5 ; } | whatpipe.sh will likely yield a bash (the input subshell) and sleep 5.
  2. +E is only available if lsof was compiled with -DHASUXSOCKEPT. That should be true for most modern Linux distros, but check your installation anyway with: lsof -v 2>&1 | grep HASUXSOCKEPT

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .