11

E.g. command line:

test.sh arg1 | grep "xyz"

Is it possible to get the complete command line including the following grep in the bash script test.sh?

hellcode
  • 742
  • can you clarify what you mean by "command line"? – Bart Jul 23 '19 at 08:42
  • I am just wondering if there is a special dollar variable that contains the complete string (the command line), not just the script name and its arguments – hellcode Jul 23 '19 at 08:46
  • 2
    What would be your use case for doing this? – Kusalananda Jul 23 '19 at 08:49
  • It is not relevant for this question. In my case I could suppress a user interaction (read) in the test script if i know the following commands of the command line. Sure, I can do this in other ways. But this is not the question. – hellcode Jul 23 '19 at 08:56
  • 9
    @hellcode you don't need to know if you're in a pipe for that. Just check if output is a TTY. [ -t 1 ] https://unix.stackexchange.com/a/401938/70524 – muru Jul 23 '19 at 09:12
  • you probably know your pid and can call the ps command, so with some tinkering it could be possible to get what you need. or maybe through the history command? just ideas, not an answer – notananswer Jul 23 '19 at 19:19
  • 1
    Relating in: https://unix.stackexchange.com/q/485271/117549 – Jeff Schaller Jul 23 '19 at 19:32

5 Answers5

14

no

bash (or your shell) will fork two distinct commands.

  1. test.sh arg1
  2. grep "xyz"

test.sh couldn't know about following grep.

you might however know you are "inside" a pipe by testing /proc/self/fd/1

test.sh

#!/bin/bash

file /proc/self/fd/1

which run as

> ./test.sh
/proc/self/fd/1: symbolic link to /dev/pts/0
> ./test.sh | cat
/proc/self/fd/1: broken symbolic link to pipe:[25544239]

(Edit) see muru’s comment about knowing if you are on a pipe.

you don't need to know if you're in a pipe for that. Just check if output is a TTY. [ -t 1 ] https://unix.stackexchange.com/a/401938/70524

wjandrea
  • 658
Archemar
  • 31,554
8

There is no way to do that in general.

But an interactive bash shell can leverage the history mechanism and the DEBUG trap to "tell" the commands it runs the complete command line they're part of via an environment variable:

$ trap 'export LC=$(fc -nl -0); LC=${LC#? }' DEBUG
$ sh -c 'printf "last_command={%s}\n" "$LC"' | cat; true
last_command={sh -c 'printf "last_command={%s}\n" "$LC"' | cat; true}
3

By using /proc/self/fd, you can see if you're in a pipeline as well as an ID for the pipe. If you iterate through /proc/\*/fd looking for the matching pipe, you can find the PID of the other end of the pipe. With the PID, you can then read /proc/$PID/cmdline as well as repeat the process on its file descriptors to find what it's piped into.

$ cat | cat | cat &
$ ps
  PID TTY          TIME CMD
 6942 pts/16   00:00:00 cat
 6943 pts/16   00:00:00 cat
 6944 pts/16   00:00:00 cat
 7201 pts/16   00:00:00 ps
20925 pts/16   00:00:00 bash
$ ls -l /proc/6942/fd
lrwx------. 1 tim tim 64 Jul 24 19:59 0 -> /dev/pts/16
l-wx------. 1 tim tim 64 Jul 24 19:59 1 -> 'pipe:[49581130]'
lrwx------. 1 tim tim 64 Jul 24 19:59 2 -> /dev/pts/16
$ ls -l /proc/6943/fd
lr-x------. 1 tim tim 64 Jul 24 19:59 0 -> 'pipe:[49581130]'
l-wx------. 1 tim tim 64 Jul 24 19:59 1 -> 'pipe:[49581132]'
lrwx------. 1 tim tim 64 Jul 24 19:59 2 -> /dev/pts/16
$ ls -l /proc/6944/fd
lr-x------. 1 tim tim 64 Jul 24 19:59 0 -> 'pipe:[49581132]'
lrwx------. 1 tim tim 64 Jul 24 19:59 1 -> /dev/pts/16
lrwx------. 1 tim tim 64 Jul 24 19:59 2 -> /dev/pts/16

Also if you're lucky, the different commands in the pipeline will get consecutive PIDs which will make it a bit easier.

I don't actually have a script to do this, but I have proved the concept.

2

Thanks for your answers. I have tested different things and came to the following test script:

test.sh:

hist=`fc -nl -0`
# remove leading and trailing whitespaces
hist="$(echo "${hist}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
echo "Command line from history: '$hist'"

if [ -t 1 ]; then
  echo "Direct output to TTY, no pipe involved."
else
  echo "No TTY, maybe a piped command."
fi

if [ -p /dev/stdout ]; then
  echo "stdout is a pipe."
else
  echo "stdout is not a pipe."
fi

readlink -e /proc/self/fd/1
rst=$?
if [ $rst -eq 0 ]; then
  echo "Readlink test status okay, no pipe involved."
else
  echo "Readlink test status error $rst, maybe a piped command."
fi

Tests:

$ ./test.sh test1
Command line from history: './test.sh test1'
Direct output to TTY, no pipe involved.
stdout is not a pipe.
/dev/pts/3
Readlink test status okay, no pipe involved.

$ ./test.sh test2 | cat
Command line from history: './test.sh test2 | cat'
No TTY, maybe a piped command.
stdout is a pipe.
Readlink test status error 1, maybe a piped command.

$ echo "another command before pipe doesn't matter" | ./test.sh test3
Command line from history: 'echo "another command before pipe doesn't matter" | ./test.sh test3'
Direct output to TTY, no pipe involved.
stdout is not a pipe.
/dev/pts/3
Readlink test status okay, no pipe involved.

The command line history is working only without a Shebang at the top line of the script. Don't know if this will work reliable and on other systems, too.

I wasn't able to suppress the output from "readlink" (or "file" like suggested from Archemar), when the status was successful ("/dev/pts/3"). Piping output to /dev/null or to a variable would lead to malfunction. So this wouldn't be an option for me in a script.

The TTY check that muru mentioned is easy and is maybe already sufficient for some use cases.

Edit: My credit goes to mosvy, because the question was how to get the complete command line and not just to determine if the script is on a pipe. I like the simple part "fc -nl -0" in his answer, because no further system configuration is needed. It is not a 100 percent solution, but this is just for my personal use and therefore sufficient. Thanks to all others for your help.

hellcode
  • 742
  • The TTY check can also be done for stdin: [ -t 0 ]. So you can check if either stdin or stdout isn't a TTY and proceed accordingly. – muru Jul 24 '19 at 02:05
  • If you want to know if the stdout is a pipe, on Linux you can use if [ -p /dev/stdout ]; ... (just like readlink /proc/self/fd/.. this does not work on BSD). –  Jul 24 '19 at 05:29
  • 2
    The script needs work IMNSHO. the echo -e almost certainly does not want the -e. You need more test cases, redirecting to a file, being invoked inside $(...). However I would urge you to consider if this is a good idea. Programs like ls which change their output depending on if they are outputting to a tty or a pipe are annoying to use. – icarus Jul 24 '19 at 06:54
1

Another way might be by accessing the $BASH_COMMAND automatic variable, but it's inherently volatile and difficult to catch the wanted value.

I think you could only catch it only via an eval, which also involves invoking your command-lines in a special way, like in:

CMD="${BASH_COMMAND##* eval }" eval './test.sh arg1 | grep "xyz"'

Here $BASH_COMMAND is expanded while also purging it of up to the eval bit of string, and the result string is thus "snapshotted" into a helper $CMD variable.

Little example:

$ cat test.sh
#!/bin/sh

printf 'you are running %s\n' "$CMD"
sleep 1
echo bye bye
$
$ CMD="${BASH_COMMAND##* eval }" eval './test.sh | { grep -nH "."; }'
(standard input):1:you are running './test.sh | { grep -nH "."; }'
(standard input):2:bye bye
$

Naturally it can also work (actually better) while invoking scripts by e.g. sh -c or bash -c, as in:

$
$ CMD="${BASH_COMMAND}" sh -c './test.sh | { grep -nH "."; }'
(standard input):1:you are running CMD="${BASH_COMMAND}" sh -c './test.sh | { grep -nH "."; }'
(standard input):2:bye bye
$

Here without purging the variable.

LL3
  • 5,418