Qual é a diferença entre “cat file | ./binary ”e“ ./binary file ”?

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. Eu não sei porque não funciona com um tubo. Em todos os 3 casos, o conteúdo do arquivo é dado à entrada padrão de binário (de maneiras diferentes):

  1. bash lê o arquivo e o dá para stdin de binário
  2. o bash lê linhas de stdin (até EOF) e dá a stdin de binário
  3. cat lê e coloca as linhas de arquivo para stdout, bash redireciona para stdin de binario

O binário não deve notar a diferença entre esses 3 até agora como 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 Could not read script file '', error code '14'.

Mas a minha principal questão é, como é que existe uma diferença para qualquer programa com essas 3 opções?

Aqui estão alguns detalhes adicionais: Eu tentei de novo com strace e houve de fato alguns erros ESPIPE (busca ilegal) de lseek seguido por EFAULT (endereço incorreto) de ler 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 de Automic (UC4) .

    
por Boris 16.01.2017 / 12:51

4 respostas

149

Em

./binary < file
O stdin de

binary é o arquivo aberto no modo somente leitura. Note que bash não lê o arquivo, apenas o abre para leitura no descritor de arquivo 0 (stdin) do processo que executa binary in.

Em:

./binary << EOF
test
EOF

Dependendo do shell, o stdin de binary será um arquivo temporário excluído (AT & T ksh, zsh, bash ...) que contém test\n como colocado lá pelo shell ou o final de leitura de um pipe ( dash , yash ; e o shell grava test\n em paralelo na outra extremidade do pipe). No seu caso, se você estiver usando bash , seria um arquivo temporário.

Em:

cat file | ./binary

Dependendo do shell, o stdin de binary será o final de leitura de um pipe ou uma extremidade de um par de soquetes onde a direção de escrita foi encerrada (ksh93) e cat está gravando o conteúdo de file no outro extremo.

Quando stdin é um arquivo regular (temporário ou não), é procurado. binary pode ir para o início ou fim, retroceder, etc. Ele também pode mapear, fazer alguns ioctl()s como FIEMAP / FIBMAP (se usar <> em vez de < , ele poderá truncar / furar nele , etc).

pipes e pares de soquetes, por outro lado, são meios de comunicação entre processos, não há muito binary a fazer além de read ing os dados (embora também existam algumas operações como ioctl() s específicas de canal que poderia fazer com eles e não em arquivos regulares).

Na maioria das vezes, é a capacidade perdida de seek que faz com que os aplicativos falhem / reclamem ao trabalhar com pipes, mas poderia ser qualquer outra chamada de sistema válida em arquivos regulares, mas não em tipos diferentes de arquivos (como mmap() , ftruncate() , fallocate() ). No Linux, também há uma grande diferença no comportamento quando você abre /dev/stdin enquanto o fd 0 está em um canal ou em um arquivo regular.

Existem muitos comandos por aí que só podem lidar com arquivos buscáveis , mas quando esse é o caso, geralmente não são os arquivos abertos no 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.

unzip precisa 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, pipe no segundo) é dado como um argumento de caminho para unzip e unzip abre ele mesmo (normalmente em fd diferente de 0) em vez de herdar um fd já aberto pelo pai. Não lê arquivos zip de seu stdin. O stdin é usado principalmente para interação do usuário.

Se você executar esse binary do seu sem redirecionamento no prompt de um shell interativo em execução em um emulador de terminal, então o stdin de binary será herdado de seu pai do 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 procurados. Portanto, se binary funcionar bem ao receber entradas do terminal, talvez o problema não seja a busca.

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

binary possivelmente determina o tipo de arquivo aberto em seu stdin (com fstat() ) e é executado em um bug quando não é um arquivo regular nem um dispositivo tty.

Difícil dizer sem saber mais sobre o aplicativo. Executá-lo em strace (ou truss / tusc equivalente em seu sistema) pode nos ajudar a ver qual é a chamada do sistema, se houver alguma falha aqui.

1 O cenário imaginado por Matthew Ife em um comentário à sua pergunta parece bastante plausível aqui. Citando ele:

I suspect it is seeking to the end of file to get a buffer size for reading the data, badly handling the fact that seek doesn't work and attempting to allocate a negative size (not handling a bad malloc). Passing the buffer to read which faults given the buffer is not valid.

    
por 16.01.2017 / 13:06
46

Aqui está um programa de exemplo simples que ilustra a resposta de Stéphane Chazelas usando lseek(2) na 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);
}

Teste:

$ 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 procurados, e esse é um lugar onde um programa pode reclamar sobre tubos.

    
por 16.01.2017 / 13:10
21

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

Especificamente, aqui está um trecho do código-fonte bash's , 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)

Assim, como o redirecionamento basicamente pode ser tratado como arquivos, os binários podem navegar neles, ou seek() pelo arquivo, pulando 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 procuráveis, ou seja, você não pode navegar livremente por eles - apenas ler sequencialmente. Uma vez eu implementei o comando tail em python. 29 milhões de linhas de texto podem ser consultadas em microssegundos se forem redirecionadas, mas se cat 'via pipe, bem, não há nada que possa ser feito - então tudo tem que ser lido sequencialmente.

Outra possibilidade é que o binário queira abrir um arquivo especificamente e não queira receber entrada de um pipe. Geralmente é feito via fstat() system call, e checando se a entrada vem de um tipo de arquivo S_ISFIFO (que significa pipe / named pipe).

Seu binário específico, já que não sabemos o que é, provavelmente tenta procurar, mas não pode procurar por canos. Recomenda-se que você consulte sua documentação para descobrir exatamente o que o código de erro 14 significa.

OBSERVAÇÃO : Alguns shells, como dash (Debian Almquist Shell, padrão /bin/sh no Ubuntu) implementam o redirecionamento here-doc com pipes internamente , portanto, podem não ser pesquisáveis. O ponto permanece o mesmo - os canais são sequenciais e não podem ser facilmente navegados, e tentativas para isso resultarão em erros.

    
por 16.01.2017 / 13:24
5

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

No seguinte caso, o erro é relatado

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

No seguinte caso, 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 !

Existe outra diferença, quando usamos funções de shell em vez de binários. Em bash , funções que fazem parte de um pipeline são executadas em sub-shells (exceto para o último componente de pipeline se a opção lastpipe estiver habilitada e bash não for interativa), portanto a mudança de variáveis não tem 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
    
por 16.01.2017 / 19:31