É possível acessar a linha de comando completa, incluindo pipes em um script bash?

8

Por exemplo, linha de comando:

test.sh arg1 | grep "xyz"

É possível obter a linha de comando completa, incluindo o seguinte grep no script bash test.sh?

código do inferno
fonte
você pode esclarecer o que você quer dizer com "linha de comando"?
23419 Bart Bart
Estou apenas me perguntando se há uma variável especial dólar que contém a cadeia completa (linha de comando), e não apenas o nome do script e seus argumentos
hellcode
2
Qual seria o seu caso de uso para fazer isso?
Kusalananda
9
@ hellcode você não precisa saber se está interessado em isso. Basta verificar se a saída é um TTY. [ -t 1 ] unix.stackexchange.com/a/401938/70524
muru
11
Relacionado em: unix.stackexchange.com/q/485271/117549
Jeff Schaller

Respostas:

6

Não há como fazer isso em geral .

Mas um bashshell interativo pode aproveitar o mecanismo do histórico e a DEBUGarmadilha para "contar" os comandos nos quais executa a linha de comando completa da qual fazem parte por meio de uma variável de ambiente:

$ 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}
mosvy
fonte
13

não

O bash (ou seu shell) bifurcará dois comandos distintos.

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

test.sh não sabia sobre seguir grep.

no entanto, você pode saber que está "dentro" de um tubo testando /proc/self/fd/1

test.sh

#!/bin/bash

file /proc/self/fd/1

que funcionam como

> ./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]

(Editar) veja o comentário de muru sobre saber se você está em um cano.

você não precisa saber se está afim disso. Basta verificar se a saída é um TTY. [ -t 1 ] https://unix.stackexchange.com/a/401938/70524

Archemar
fonte
Embora útil, este só funciona em Linux - não outra Unixes
Scott Earle
2

Ao usar /proc/self/fd, você pode ver se está em um pipeline e também um ID para o pipe. Se você /proc/\*/fdprocurar o tubo correspondente, poderá encontrar o PID da outra extremidade do tubo. Com o PID, você pode ler /proc/$PID/cmdlinee repetir o processo em seus descritores de arquivo para descobrir o que é canalizado.

$ 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

Além disso, se você tiver sorte, os diferentes comandos no pipeline receberão PIDs consecutivos, o que facilitará um pouco.

Na verdade, não tenho um script para fazer isso, mas provei o conceito.

Tim Anderson
fonte
1

Outra maneira pode ser acessando a $BASH_COMMANDvariável automática, mas é inerentemente volátil e difícil de capturar o valor desejado.

Eu acho que você só poderia pegá-lo através de um eval, o que também envolve chamar suas linhas de comando de uma maneira especial, como em:

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

Aqui, ele $BASH_COMMANDé expandido enquanto o expurga até o evalbit da string, e a string do resultado é "capturada instantaneamente" em uma $CMDvariável auxiliar .

Pequeno exemplo:

$ 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
$

Naturalmente, também pode funcionar (na verdade melhor) ao invocar scripts por exemplo sh -cou bash -c, como em:

$
$ 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
$

Aqui sem limpar a variável.

LL3
fonte
1

Obrigado por suas respostas. Eu testei coisas diferentes e vim para o seguinte script de teste:

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

Testes:

$ ./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.

O histórico da linha de comando está funcionando apenas sem um Shebang na linha superior do script. Não sei se isso funcionará de maneira confiável e em outros sistemas também.

Não consegui suprimir a saída do "readlink" (ou "arquivo", como sugerido pela Archemar), quando o status foi bem-sucedido ("/ dev / pts / 3"). A saída da tubulação para / dev / null ou para uma variável levaria ao mau funcionamento. Portanto, isso não seria uma opção para mim em um script.

A verificação TTY mencionada por muru é fácil e talvez já seja suficiente para alguns casos de uso.

Edit: Meu crédito vai para mosvy, porque a questão era como obter a linha de comando completa e não apenas para determinar se o script está em um cano. Eu gosto da parte simples "fc -nl -0" em sua resposta, porque nenhuma configuração adicional do sistema é necessária. Não é uma solução 100%, mas é apenas para meu uso pessoal e, portanto, suficiente. Obrigado a todos pela ajuda.

código do inferno
fonte
A verificação TTY também pode ser feito para stdin: [ -t 0 ]. Portanto, você pode verificar se stdin ou stdout não é um TTY e proceder de acordo.
Muru
Se você quiser saber se o stdout é um pipe, no Linux você pode usar if [ -p /dev/stdout ]; ...(assim readlink /proc/self/fd/..não funciona no BSD).
mosvy
2
O script precisa funcionar IMNSHO. o echo -equase certamente não quer o -e. Você precisa de mais casos de teste, redirecionando para um arquivo, sendo chamado por dentro $(...). No entanto, exorto-o a considerar se é uma boa ideia. Programas como os lsque alteram sua saída, dependendo se estão sendo enviados para um tty ou um pipe, são irritantes de usar.
icarus