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?
no
bash (or your shell) will fork two distinct commands.
test.sh arg1
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
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}
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.
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.
[ -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 [ -p /dev/stdout ]; ...
(just like readlink /proc/self/fd/..
this does not work on BSD).
–
Jul 24 '19 at 05:29
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
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.
[ -t 1 ]
https://unix.stackexchange.com/a/401938/70524 – muru Jul 23 '19 at 09:12