Como verificar se um tubo está vazio e executar um comando nos dados, se não estiver?

42

Eu canalizei uma linha no script bash e quero verificar se o pipe possui dados antes de enviá-lo para um programa.

Pesquisando eu encontrei, test -t 0mas não funciona aqui. Sempre retorna falso. Então, como ter certeza de que o tubo possui dados?

Exemplo:

echo "string" | [ -t 0 ] && echo "empty" || echo "fill"

Saída: fill

echo "string" | tail -n+2 | [ -t 0 ] && echo "empty" || echo "fill"

Saída: fill

Ao contrário da maneira canônica / padrão de testar se o pipeline anterior produziu saída? a entrada precisa ser preservada para transmiti-la ao programa. Isso generaliza Como canalizar a saída de um processo para outro, mas apenas executar se o primeiro tiver saída? que se concentra no envio de e-mail.

zetah
fonte
Muito parecido: Conduza um tubo condicional ao retorno não vazio (no superusuário )
G-Man diz 'Reinstate Monica'

Respostas:

37

Não há como espiar o conteúdo de um canal usando utilitários shell geralmente disponíveis, nem existe uma maneira de ler um caractere no canal e colocá-lo de volta. A única maneira de saber que um canal tem dados é ler um byte e, em seguida, você deve levá-lo ao seu destino.

Faça exatamente isso: leia um byte; se você detectar um final de arquivo, faça o que deseja fazer quando a entrada estiver vazia; se você ler um byte, bifurque o que você quer fazer quando a entrada não estiver vazia, canalize esse byte nele e canalize o restante dos dados.

first_byte=$(dd bs=1 count=1 2>/dev/null | od -t o1 -A n | tr -dc 0-9)
if [ -z "$first_byte" ]; then
  # stuff to do if the input is empty
else
  {
    printf "\\$first_byte"
    cat
  } | {
    # stuff to do if the input is not empty
  }      
fi

O ifneutilitário do moreutils de Joey Hess executa um comando se sua entrada não estiver vazia. Geralmente não é instalado por padrão, mas deve estar disponível ou fácil de construir na maioria das variantes unix. Se a entrada estiver vazia, ifnenada fará e retornará o status 0, que não pode ser diferenciado do comando em execução com sucesso. Se você desejar fazer algo se a entrada estiver vazia, será necessário organizar o comando para não retornar 0, o que pode ser feito com o caso de sucesso retornando um status de erro distinto:

ifne sh -c 'do_stuff_with_input && exit 255'
case $? in
  0) echo empty;;
  255) echo success;;
  *) echo failure;;
esac

test -t 0não tem nada a ver com isso; ele testa se a entrada padrão é um terminal. Não diz nada de uma maneira ou de outra sobre se alguma entrada está disponível.

Gilles 'SO- parar de ser mau'
fonte
Em sistemas com tubos baseados em STREAMS (Solaris HP / UX), acredito que você possa usar o I_PEEK ioctl para espiar o que está em um tubo sem consumi-lo.
Stéphane Chazelas 29/03
@ StéphaneChazelas, infelizmente, não há como obter dados de um pipe / fifo no * BSD, então não há perspectiva de implementar um utilitário portátil peek que possa retornar os dados reais de um pipe, e não apenas quanto dele existe. (em 4.4 tubos BSD, 386BSD etc foram implementados como pares de soquetes , mas isso foi destruído em versões posteriores do * BSD - embora eles os mantivessem bidirecionais).
mosvy 29/03
O bash possui uma rotina para verificar as entradas expostas por a read -t 0(t nesse caso significa tempo limite, se você se perguntar).
Isaac
11

Uma solução simples é usar o ifnecomando (se a entrada não estiver vazia). Em algumas distribuições, ele não é instalado por padrão. É uma parte do pacote moreutilsna maioria das distros.

ifne executa um determinado comando se e somente se a entrada padrão não estiver vazia

Observe que, se a entrada padrão não estiver vazia, ela será passada ifnepara o comando especificado

Nick Wirth
fonte
2
A partir de 2017, não está lá por padrão no Mac ou no Ubuntu.
Sridhar Sarnobat
7

Pergunta antiga, mas no caso de alguém se deparar com isso, como eu fiz: minha solução é ler com um tempo limite.

while read -t 5 line; do
    echo "$line"
done

Se stdinestiver vazio, isso retornará após 5 segundos. Caso contrário, ele lerá toda a entrada e você poderá processá-la conforme necessário.

Ladd
fonte
Embora eu goste da ideia, -tinfelizmente não faz parte do POSIX: pubs.opengroup.org/onlinepubs/9699919799/utilities/read.html
JepZ
6

verifique se o descritor de arquivo stdin (0) está aberto ou fechado:

[ ! -t 0 ] && echo "stdin has data" || echo "stdin is empty"
mviereck
fonte
Quando você passa alguns dados e deseja verificar se há alguns, você passa no FD de qualquer maneira, portanto, esse também não é um bom teste.
Jakuje 31/01
11
[ -t 0 ]verifica se fd 0 está aberto para um tty , não se está fechado ou aberto.
mosvy
@mosvy, você poderia explicar como isso afetaria o uso dessa solução em um script? Existem casos em que não funciona?
JepZ 22/04
@JepZ huh? ./that_script </dev/null=> "stdin tem dados". Ou ./that_script <&-para ter o stdin realmente fechado .
mosvy 22/04
5

Você pode usar test -s /dev/stdin(em um subshell explícito) também.

# test if a pipe is empty or not
echo "string" | 
    (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')

echo "string" | tail -n+2 | 
    (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')

: | (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')
trent55
fonte
8
Não funciona para mim. Sempre diz que o tubo está vazio.
Amphetamachine
2
Funciona no meu Mac, mas não na minha caixa Linux.
peak
3

Na festança:

read -t 0 

Detecta se uma entrada possui dados (sem ler nada). Então você pode ler a entrada (se a entrada estiver disponível no momento em que a leitura é executada):

if     read -t 0
then   read -r input
       echo "got input: $input"
else   echo "No data to read"
fi

Nota: Entenda que isso depende do tempo. Isso detecta se a entrada já possui dados apenas no momento da read -texecução.

Por exemplo, com

{ sleep 0.1; echo "abc"; } | read -t 0; echo "$?"

a saída é 1(falha na leitura, ou seja: entrada vazia). O eco grava alguns dados, mas não é muito rápido para iniciar e gravar seu primeiro byte, portanto, read -t 0relatará que sua entrada está vazia, pois o programa ainda não escreveu nada.

Isaac
fonte
github.com/bminor/bash/blob/… - aqui está a fonte de como o bash detecta que algo está no descritor de arquivo.
Pavel Patrin 12/06
Obrigado @PavelPatrin
Isaac
11
@PavelPatrin Isso não funciona . Como pode ser visto claramente no seu link, bashele fará um select()ou um ioctl(FIONREAD)deles, ou nenhum deles, mas não ambos, como deve funcionar. read -t0está quebrado. Não o use
mosvy
Oooh, hoje eu tento entender o que há de errado com isso por duas horas! Obrigado, @mosvy!
Pavel Patrin
3

Uma maneira fácil de verificar se há dados disponíveis para leitura no Unix é com o FIONREADioctl.

Não consigo pensar em nenhum utilitário padrão fazendo exatamente isso, então aqui está um programa trivial (melhor do que o ifnefromutils IMHO ;-)).

fionread [ prog args ... ]

Se não houver dados disponíveis no stdin, ele sairá com o status 1. Se houver dados, ele será executado prog. Se não progfor fornecido, ele sairá com o status 0.

Você pode remover a pollchamada se estiver interessado apenas nos dados disponíveis imediatamente . Isso deve funcionar com a maioria dos tipos de fds, não apenas com pipes.

fionread.c

#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#ifdef __sun
#include <sys/filio.h>
#endif
#include <err.h>

int main(int ac, char **av){
        int r; struct pollfd pd = { 0, POLLIN };
        if(poll(&pd, 1, -1) < 0) err(1, "poll");
        if(ioctl(0, FIONREAD, &r)) err(1, "ioctl(FIONREAD)");
        if(!r) return 1;
        if(++av, --ac < 1) return 0;
        execvp(*av, av);
        err(1, "execvp %s", *av);
}
mosvy
fonte
Este programa realmente funciona? Você não precisa esperar por um POLLHUPevento também para lidar com o caso vazio? Isso funciona se houver várias descrições de arquivo na outra extremidade do canal?
Gilles 'SO- stop be evil'
Sim, funciona. POLLHUP é retornado apenas por pesquisa, você deve usar POLLIN para aguardar um POLLHUP. Não importa quantas alças abertas estejam em qualquer extremidade do tubo.
mosvy 29/03
Veja unix.stackexchange.com/search?q=FIONREAD+user%3A22565 para saber como executar o FIONREAD a partir do perl (mais comumente disponível que compiladores)
Stéphane Chazelas
A rotina que usa FIONREAD (ou HAVE_SELECT) é implementada no bash aqui .
Isaac
2

Se você gosta de frases curtas e enigmáticas:

$ echo "string" | grep . && echo "fill" || echo "empty"
string
fill
$ echo "string" | tail -n+2 | grep . && echo "fill" || echo "empty"
empty

Eu usei os exemplos da pergunta original. Se você não deseja que os dados canalizados usem a -qopção grep

yashma
fonte
0

Esta parece ser uma implementação ifne razoável no bash, se você estiver de acordo em ler a primeira linha inteira

ifne () {
        read line || return 1
        (echo "$line"; cat) | eval "$@"
}


echo hi | ifne xargs echo hi =
cat /dev/null | ifne xargs echo should not echo
Eric Woodruff
fonte
5
readtambém retornará false se a entrada não estiver vazia, mas não contiver caracteres de nova linha, realizar readalgum processamento em sua entrada e poderá ler mais de uma linha, a menos que você a chame como IFS= read -r line. echonão pode ser usado para dados arbitrários.
Stéphane Chazelas
0

Isso funciona para mim usando read -rt 0

exemplo da pergunta original, sem dados:

echo "string" | tail -n+2 | if read -rt 0 ; then echo has data ; else echo no data ; fi
user333755
fonte
não, isso não funciona. tente com { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }(falso negativo) e true | { sleep .1; read -rt0 && echo YES; }(falso positivo). Na verdade, a festança da readserá enganado até mesmo por fds abertas em somente gravação modo: { read -rt0 && echo YES; cat; } 0>/tmp/foo. A única coisa que parece fazer é um select(2)nesse fd.
mosvy
... e selectretornará um fd como "pronto" se um read(2)bloco não bloquear, não importa se retornará EOFou ocorrerá um erro. Conclusão: read -t0é quebrado em bash. Não use.
mosvy
@mosvy Você já denunciou isso com o bashbug?
Isaac
@mosvy O { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }não é um falso negativo porque (no momento em que a leitura é executada) não há entrada. Mais tarde (suspensão .1), essa entrada está disponível (para o gato).
Isaac
@mosvy Por que a opção r afeta a detecção ?: echo "" | { read -t0 && echo YES; }imprime SIM, mas echo "" | { read -rt0 && echo YES; }não.
Isaac