Qual é a diferença entre “cat file | ./binary "e" ./binary <arquivo "?

102

Eu tenho um binário (que não posso modificar) e posso fazer:

./binary < file

Eu também posso fazer:

./binary << EOF
> "line 1 of file"
> "line 2 of file"
...
> "last line of file"
> EOF

Mas

cat file | ./binary

me dá um erro. Não sei por que não funciona com um cano. Nos três casos, o conteúdo do arquivo é dado à entrada padrão do binário (de maneiras diferentes):

  1. bash lê o arquivo e o entrega ao stdin de binário
  2. bash lê linhas de stdin (até EOF) e dá a stdin de binário
  3. gato lê e coloca as linhas de arquivo em stdout, o bash as redireciona para stdin of binary

O binário não deve notar a diferença entre os três, tanto quanto eu entendi. Alguém pode explicar por que o terceiro caso não funciona?

BTW: O erro dado pelo binário é:

20170116 / 125624.689 - U3000011 Não foi possível ler o arquivo de script '', código de erro '14'.

Mas minha pergunta principal é: como existe uma diferença para qualquer programa com essas três opções.

Aqui estão alguns detalhes adicionais: tentei novamente com strace e, de fato, ocorreram alguns erros ESPIPE (busca ilegal) de lseek, seguidos por EFAULT (endereço incorreto) da leitura logo antes da mensagem de erro.

O binário que tentei controlar com um script ruby ​​(sem usar arquivos temporários) faz parte do callapi da Automic (UC4) .

Boris
fonte
25
Legal, existe um detector UUOC embutido no seu binário. Quero isso.
xhienne
4
Qual é o SO (para que possamos dizer o que é 14, se é para ser um erro)?
Stéphane Chazelas
6
Mesmo que seja possível que um programa reaja dessa maneira, seria um programa com uma aparência estranha. Todo programa não-louco que espera qualquer entrada do stdin precisa funcionar quando o stdin é um tty, e se ele pode funcionar com um tty e um arquivo, há poucas razões para não suportar também pipes. Provavelmente, o autor do programa teve uma hemorragia temporária e, embora que qualquer coisa que isatty()retorna false para será um arquivo pesquisável ou mmappable ...
Henning Makholm
9
O código de erro 14 significa EFAULT. Em uma leitura que ocorre se o buffer que você declarou é inválido. Eu rastrearia o programa, mas suspeito que ele esteja procurando no final do arquivo obter um tamanho de buffer para ler os dados, lidando mal com o fato de que o query não funciona e tentando alocar um tamanho negativo (não lidando com um malloc ruim) . Passar o buffer para ler quais falhas, dadas o buffer, não são válidas.
Matthew Ife
3
@xhienne Não, tem um preventor cat. Parece que você não pode usá-lo para combinar dois arquivos, como é o uso pretendido.
Jpmc26

Respostas:

150

No

./binary < file

binarystdin é o arquivo aberto no modo somente leitura. Observe que bashele não lê o arquivo, apenas o abre para leitura no descritor de arquivo 0 (stdin) do processo em que ele é executado binary.

No:

./binary << EOF
test
EOF

Dependendo do shell, binaryo stdin será um arquivo temporário excluído (AT&T ksh, zsh, bash ...) que contenha test\ncomo colocado ali pelo shell ou pela extremidade de leitura de um pipe ( dash, yash; e o shell grava test\nem paralelo na outra extremidade do tubo). No seu caso, se você estiver usando bash, seria um arquivo temporário.

No:

cat file | ./binary

Dependendo do shell, binaryo stdin será a extremidade de leitura de um tubo ou a extremidade de um par de soquetes em que a direção de gravação foi desativada (ksh93) e catestá gravando o conteúdo da fileoutra extremidade.

Quando stdin é um arquivo regular (temporário ou não), ele pode ser procurado. binarypode ir para o início ou o fim, retroceder, etc. Também pode mapeá-lo, fazer alguns ioctl()scomo FIEMAP / FIBMAP (se usar em <>vez de <, poderá truncar / fazer furos nele, etc.).

pares de tubos e soquetes, por outro lado, são um meio de comunicação entre processos, não há muito o binaryque fazer além readdos dados (embora também existam algumas operações, como alguns ioctl()s específicos de um tubo, que eles poderiam fazer neles e não em arquivos regulares) .

Na maioria das vezes, é a capacidade que faltava para seekque faz com que aplicativos para falhar / reclamar quando se trabalha com tubos, mas poderia ser qualquer uma das outras chamadas de sistema que são válidas em arquivos regulares, mas não em diferentes tipos de arquivos (como mmap(), ftruncate(), fallocate()) . No Linux, também há uma grande diferença de comportamento quando você abre /dev/stdinenquanto o fd 0 está em um pipe ou em um arquivo regular.

Existem muitos comandos por aí que só podem lidar com arquivos pesquisáveis , mas quando esse é o caso, geralmente não é para os arquivos abertos em seu stdin.

$ unzip -l file.zip
Archive:  file.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       11  2016-12-21 14:43   file
---------                     -------
       11                     1 file
$ unzip -l <(cat file.zip)
     # more or less the same as cat file.zip | unzip -l /dev/stdin
Archive:  /proc/self/fd/11
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of /proc/self/fd/11 or
        /proc/self/fd/11.zip, and cannot find /proc/self/fd/11.ZIP, period.

unzipprecisa ler o índice armazenado no final do arquivo e, em seguida, procurar dentro do arquivo para ler os membros do arquivo. Mas aqui, o arquivo (regular no primeiro caso, canal no segundo) é fornecido como argumento de caminho unzipe o unzipabre por si próprio (normalmente em fd diferente de 0) em vez de herdar um fd já aberto pelo pai. Ele não lê arquivos zip de seu stdin. stdin é usado principalmente para interação do usuário.

Se você executar o binaryseu sem redirecionamento no prompt de um shell interativo em execução em um emulador de terminal, binaryo stdin será herdado de seu pai, o shell, que por sua vez o herdará de seu pai, o emulador de terminal e será um dispositivo pty aberto no modo de leitura + gravação (algo como /dev/pts/n).

Esses dispositivos também não são procuráveis. Portanto, se binaryfuncionar bem ao receber informações do terminal, possivelmente o problema não é procurar.

Se esse número 14 for um erro (um código de erro definido por falhas nas chamadas do sistema), na maioria dos sistemas, isso seria EFAULT( Endereço incorreto ). A read()chamada do sistema falharia com esse erro se solicitada a leitura em um endereço de memória que não seja gravável. Isso seria independente de o fd ler os dados dos pontos em um canal ou arquivo regular e geralmente indicaria um erro 1 .

binarypossivelmente determina o tipo de arquivo aberto em seu stdin (with fstat()) e se depara com um erro quando não é um arquivo comum nem um dispositivo tty.

Difícil dizer sem saber mais sobre o aplicativo. Executá-lo sob strace(ou truss/ tuscequivalente em seu sistema) pode ajudar-nos a ver o que é a chamada de sistema se houver que está a falhar aqui.


1 O cenário previsto por Matthew Ife em um comentário à sua pergunta parece muito plausível aqui. Citando-o:

Suspeito que ele esteja buscando no final do arquivo obter um tamanho de buffer para ler os dados, lidando mal com o fato de que a busca não funciona e tentando alocar um tamanho negativo (não lidando com um malloc ruim). Passar o buffer para ler quais falhas, dadas o buffer, não são válidas.

Stéphane Chazelas
fonte
14
Muito interessante ... esta é a primeira vez que ouvi dizer que ./binary < fileé possível procurar informações redirecionadas no estilo de !
David Z
2
O @DavidZ é um arquivo openeditado e se comporta da mesma forma que qualquer arquivo openeditado. Acontece que ele foi herdado de um processo pai, mas isso não é tão incomum.
Hbbs
3
Se o sistema contiver strace ou uma ferramenta similar, ele poderá ser usado para verificar em qual chamada do sistema o binário falha.
precisa saber é
2
"Ele também pode truncá-lo, mmapá-lo, fazer furos nele etc." - Bem não. O arquivo é aberto no modo somente leitura. O programa teria que abri-lo no modo de gravação para fazer isso. Mas ele não pode abri-lo no modo de gravação, porque não existe uma interface para fazer isso diretamente, nem existe uma interface para localizar "a" entrada de diretório que corresponde a um arquivo aberto (e se houver dois dentries ou zero?) . Teria que declarar o arquivo e verificar o sistema de arquivos em busca de um objeto com o mesmo número de inode. Isso seria extraordinariamente lento.
Kevin
1
@ StéphaneChazelas: tudo bem, open("/proc/self/fd/0", O_RDWR)funciona, mesmo em arquivos excluídos. Parvo eu: P. echo foo>foo; (sleep 0.5; ll -L /proc/self/fd/0; strace ./a.out; ll -L /proc/self/fd/0) < foo & sleep 0.1 && rm foounlinks fooantes do a.out ser executado com o stdin redirecionado de foo.
Peter Cordes
46

Aqui está um exemplo de programa simples que ilustra a resposta de Stéphane Chazelas usando lseek(2)sua entrada:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    int c;
    off_t off;
    off = lseek(0, 10, SEEK_SET);
    if (off == -1)
    {
        perror("Error");
        return -1;
    }
    c = getchar();
    printf("%c\n", c);
}

Testando:

$ make seek
cc     seek.c   -o seek
$ cat foo
abcdefghijklmnopqrstuwxyz
$ ./seek < foo
k
$ ./seek <<EOF
> abcdefghijklmnopqrstuvwxyz
> EOF
k
$ cat foo | ./seek
Error: Illegal seek

Tubos não são procuráveis, e esse é um lugar em que um programa pode reclamar de tubos.

muru
fonte
21

O pipe e o redirecionamento são animais diferentes, por assim dizer. Quando você usa o here-docredirecionamento ( <<) ou o stdin, < o texto não sai do nada - ele realmente entra em um descritor de arquivo (ou arquivo temporário, se você preferir), e é aí que o stdin do binário estará apontando.

Especificamente, aqui está um trecho do bash'scódigo-fonte, arquivo redir.c (versão 4.3):

/* Create a temporary file holding the text of the here document pointed to
   by REDIRECTEE, and return a file descriptor open for reading to the temp
   file.  Return -1 on any error, and make sure errno is set appropriately. */
static int
here_document_to_fd (redirectee, ri)

Portanto, como o redirecionamento pode ser basicamente tratado como arquivos, os binários podem navegá-los ou seek()através do arquivo facilmente, saltando para qualquer byte do arquivo.

Pipes, já que são buffers de 64 KiB (pelo menos no Linux) com gravações de 4096 bytes ou menos garantidos como atômicos, não são possíveis, ou seja, você não pode navegar livremente por eles - apenas lê sequencialmente. Uma vez eu implementei o tailcomando em python. 29 milhões de linhas de texto podem ser pesquisadas em microssegundos se forem redirecionadas, mas se forem catenviadas por canal, não há nada que possa ser feito - portanto, tudo deve ser lido em sequência.

Outra possibilidade é que o binário deseje abrir um arquivo especificamente e não queira receber entrada de um canal. Geralmente, isso é feito por meio de uma fstat()chamada do sistema e verificando se a entrada é proveniente de um S_ISFIFOtipo de arquivo (que significa um canal / canal nomeado).

Seu binário específico, como não sabemos o que é, provavelmente tenta procurar, mas não pode procurar tubos. É recomendável que você consulte sua documentação para descobrir o que exatamente significa o código de erro 14.

NOTA : Alguns shells, como o dash (Debian Almquist Shell, padrão /bin/shno Ubuntu) implementam here-docredirecionamento com pipes internamente , portanto, podem não ser procuráveis. O ponto permanece o mesmo - os pipes são seqüenciais e não podem ser navegados facilmente, e as tentativas resultarão em erros.

Sergiy Kolodyazhnyy
fonte
A resposta de Stephane diz que os documentos aqui podem ser implementados com tubos e que algumas conchas comuns como o dashfazem. Essa resposta explica o comportamento observado com o bash, mas esse comportamento aparentemente não é garantido por outras conchas.
Peter Cordes
@ PeterCordes é absolutamente verdade, e eu acabei de verificar isso dashno meu sistema. Eu não estava ciente disso anteriormente. Obrigado por apontar
Sergiy Kolodyazhnyy
Outro comentário: você usaria o fstat()stdin para verificar se é um cachimbo. statleva um nome de caminho. Mas, na verdade, apenas tentar lseeké provavelmente a maneira mais sensata de determinar se um fd é procurável depois de já estar aberto.
Peter Cordes
5

A principal diferença está no tratamento de erros.

No caso a seguir, o erro é relatado

$ /bin/cat < z.txt
-bash: z.txt: No such file or directory
$ echo $?
1

No caso a seguir, o erro não é relatado.

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo $?
0

Com o bash, você ainda pode usar o PIPESTATUS:

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo ${PIPESTATUS[0]}
1

Mas está disponível apenas imediatamente após a execução do comando:

$ cat z.txt | /bin/cat
cat: z.txt: No such file or directory
$ echo $?
0
$ echo ${PIPESTATUS[0]}
0
# oops !

Há outra diferença, quando usamos funções shell em vez de binários. Em bash, as funções que fazem parte de um pipeline são executadas em subcascas (exceto o último componente do pipeline, se a lastpipeopção estiver ativada e bashnão for interativa), portanto, a alteração de variáveis ​​não terá efeitos no shell pai:

$ a=a
$ b=b
$ x(){ a=x;}
$ y(){ b=y;}

$ echo $a $b
a b

$ x | y
$ echo $a $b
a b

$ cat t.txt | y
$ echo $a $b
a b

$ x | cat
$ echo $a $b
a b

$ x < t.txt
$ y < t.txt
$ echo $a $b
x y
Vouze
fonte
4
Então, você está mostrando que o tratamento de erros >é feito pelo shell, mas com o pipe é feito pelo comando que produz texto. ESTÁ BEM. Mas nesta questão específica, o OP está usando um arquivo existente, portanto esse não é o problema, e claramente o erro produzido é pelo binário.
Sergiy Kolodyazhnyy
1
Embora isso não aconteça, essa resposta tem alguma relevância para essas perguntas e respostas no caso geral e está correta, então não acho que mereça esses votos negativos.
Stéphane Chazelas
@ Berg: Quando você usa o shell como uma linha de comando, isso não é importante. Mas em scripts, o tratamento de erros pode ser muito importante.
precisa saber é