Passa o stdin para múltiplos programas sem fechar

1

Eu quero dividir e GZip um arquivo grande, e esta resposta parece ser o que eu estou procurando, e parecia uma maneira muito útil de fazer coisas que eu nunca pensei, então eu gostaria de generalizá-lo; o único problema é: não parece funcionar.

Digamos que eu queira dividir minha entrada e processá-la ainda mais (eu sei split , mas quero canalizá-la diretamente no meu script!)

Isso usa read para ler uma linha em uma variável

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
  echo "<< $i >>"
  for ((j = 0 ; j < 2 ; j++)) ; do
    read l
    echo "$l"
  done
done

Imprime

<< 0 >>
a
b
<< 1 >>
c
d

Qual é quase o que eu quero, além do fato de que apara os espaços do início e do fim (e talvez modifique a linha de outras maneiras? Ela funcionará com conteúdo codificado arbitrariamente UTF-8?) edit resolvido

E eu imagino que seja muito lento. edit Faça um benchmarking: pelo menos 3000 vezes mais lento.

Então eu tentei canalizar através de head (eu obtenho o resultado usando awk como a resposta sugere, não parece fazer nada diferente)

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
  echo "<< $i >>"
  head -n 2
done

Isso imprime

<< 0 >>
 a 
 b 
<< 1 >>

E pára porque head aparentemente fecha sua entrada na saída. Eu não encontrei um programa que não faz isso, e talvez seja realmente imposto pelo sistema? (Eu estou no OS X)

Usar head -n 2 <&0 , que (de acordo com os documentos bash) copia o descritor de arquivo, também não funciona.

Eu tenho que usar um pipe nomeado? Existe algum encantamento para fazer isso funcionar?

    
por pascal 30.07.2014 / 16:35

3 respostas

1

O problema aqui não é exatamente que head ou awk estão "fechando a entrada". Eles não têm escolha; qualquer programa fecha sua entrada quando termina, e isso é imposto pelo sistema operacional.

A questão é que a entrada padrão é um pipe, e os programas estão fazendo leituras em buffer. Não é possível não ler de um pipe, portanto, os dados que estiverem no readahead desaparecerão. Se, em vez de usar um pipe, você usar um arquivo, provavelmente verá que funciona bem:

#!/bin/bash
printf " %s \n" a b c d > /tmp/abcd
for ((i = 0 ; i < 2 ; i++)) ; do
    echo "<< $i >>"
    for ((j = 0 ; j < 2 ; j++)) ; do
        read
        echo "$REPLY"
    done
done < /tmp/abcd

Pelo menos, isso funciona bem no Ubuntu. Você pode fazê-lo funcionar com um cano se você desativar o armazenamento em buffer - mas isso provavelmente tornará as coisas realmente lentas. Aqui está um pequeno programa em C que desativa o armazenamento em buffer e depois ecoa seu caractere de entrada por caractere até consumir o número solicitado de linhas:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
  int n = 1000;
  if (argc > 1) n = atoi(argv[1]);
  setvbuf(stdin, NULL, _IONBF, 0);
  for (int ch = getchar(); ch != EOF; ch = getchar()) {
    putchar(ch);
    if (ch == '\n' && --n <= 0) break;
  }
  return n > 0;
}

Isso funcionou bem para mim (no Ubuntu, novamente - e você precisa compilá-lo com -std=c99 ou -std=c11 para que o compilador não reclame). É verdade que o programa não chama fclose(stdin) , mas adicionar não fará qualquer diferença. Por outro lado, remover a chamada para setvbuf provavelmente retornará ao sintoma que você observou com head . (E também fará com que o programa execute um lote mais rápido.)

Se você tivesse o GNU split em vez da versão BSD que acompanha o OS X, você poderia usar a sintaxe --filter=COMMAND útil, que faz muito bem exatamente o que você quer; em vez de criar arquivos divididos, ele canaliza cada seção do arquivo para uma chamada do COMMAND especificado (e define a variável de ambiente $FILE para o nome do arquivo esperado).

    
por 31.07.2014 / 07:48
1

Ao especificar uma variável para read , ordene-a para realizar uma divisão de palavras. Não faça isso e os espaços permanecerão intactos:

#!/bin/bash
printf " a \n b \n c \n d " |
for ((i = 0 ; i < 2 ; i++)) ; do
    echo "<< $i >>"
    for ((j = 0 ; j < 2 ; j++)) ; do
        read
        echo "$REPLY"
    done
done

Saída:

<< 0 >>
 a  
 b  
<< 1 >>
 c  
 d  

Parece ser muito simples, mas na verdade você fez uma pergunta muito boa, pois esse recurso não é explicado claramente pelo homem.

S. Eu usaria um sinalizador -r (não trate \ como escape char) para read também.

    
por 31.07.2014 / 00:07
0

Mas se você quiser escrever um script independente para operar arquivos grandes, o AWK seria muito mais adequado do que o Bash por motivos de eficiência. Um one-liner:

$ awk 'NR%2 { print "<< " int(NR/2) " >>" }; 1' <<< $' a \n b \n c \n d '
<< 0 >>
 a 
 b 
<< 1 >>
 c 
 d 

O mesmo que um script:

#!/usr/bin/awk -f

# where (number of line) mod 2 == 1, i. e. every odd line
NR%2 == 1 {
    # print (number of line) div 2
    print "<< " int(NR/2) " >>"
}

{  
    # print input stream
    print
} 

O mesmo que um script Bash:

#!/bin/bash

while read; do
    let lnum++
    ((lnum % 2 == 1)) && \
        echo "<< $((lnum / 2)) >>"
    echo "$REPLY"
done

Uma referência com um milhão de linhas:

$ awk 'BEGIN { for (i=1; i<=10^6; i++) print i }' >> 1e6

$ time ./pascal.awk < 1e6 > /dev/null

real    0m0.663s
user    0m0.656s
sys     0m0.004s

$ time ./pascal.sh < 1e6 > /dev/null

real    0m31.293s
user    0m29.410s
sys     0m1.852s

Você vê por que o Bash não é um intérprete preferível aqui.

    
por 31.07.2014 / 00:55

Tags