Grep do final de um arquivo para o início

32

Eu tenho um arquivo com cerca de 30.000.000 de linhas (Radius Accounting) e preciso encontrar a última correspondência de um determinado padrão.

O comando:

tac accounting.log | grep $pattern

fornece o que eu preciso, mas é muito lento porque o sistema operacional precisa primeiro ler o arquivo inteiro e enviá-lo para o canal.

Então, preciso de algo rápido que possa ler o arquivo da última linha para a primeira.

    
por Hábner Costa 02.02.2014 / 16:38

4 respostas

34

tac só ajuda se você também usar grep -m 1 (assumindo que o GNU grep ) tenha grep parado após a primeira correspondência:

tac accounting.log | grep -m 1 foo

De man grep :

   -m NUM, --max-count=NUM
          Stop reading a file after NUM matching lines.  

No exemplo da sua pergunta, tac e grep precisam processar o arquivo inteiro, então usar tac é meio que inútil.

Portanto, a menos que você use grep -m , não use tac , apenas analise a saída de grep para obter a última correspondência:

grep foo accounting.log | tail -n 1 

Outra abordagem seria usar o Perl ou qualquer outra linguagem de script. Por exemplo (onde $pattern=foo ):

perl -ne '$l=$_ if /foo/; END{print $l}' file

ou

awk '/foo/{k=$0}END{print k}' file
    
por 02.02.2014 / 17:49
10

A razão pela qual

tac file | grep foo | head -n 1

não pára na primeira partida por causa do buffer.

Normalmente, head -n 1 sai depois de ler uma linha. Portanto, grep deve obter um SIGPIPE e sair assim que escrever sua segunda linha.

Mas o que acontece é que, como sua saída não está indo para um terminal, grep a armazena. Isto é, não está escrevendo até que tenha acumulado o suficiente (4096 bytes no meu teste com o GNU grep).

O que isso significa é que grep não sairá antes de ter escrito 8192 bytes de dados, então provavelmente algumas linhas.

Com o GNU grep , você pode fazê-lo sair mais cedo usando --line-buffered , que diz para ele escrever linhas assim que forem encontradas, independentemente de ir para um terminal ou não. Então, grep sairia na segunda linha que encontrar.

Mas com o GNU grep de qualquer maneira, você pode usar -m 1 como @terdon mostrou, o que é melhor já que sai na primeira partida.

Se o seu grep não for o GNU grep , você poderá usar sed ou awk . Mas tac sendo um comando GNU, duvido que você encontre um sistema com tac onde grep não é GNU grep .

tac file | sed "/$pattern/!d;q"                             # BRE
tac file | P=$pattern awk '$0 ~ ENVIRON["P"] {print; exit}' # ERE

Alguns sistemas têm tail -r para fazer o mesmo que o GNU tac .

Observe que, para arquivos regulares (pesquisáveis), tac e tail -r são eficientes porque eles leram os arquivos para trás, eles não estão apenas lendo o arquivo totalmente na memória antes de imprimi-lo (como @sentação sed do slm ou tac em arquivos não regulares).

Em sistemas em que nem tac nem tail -r estão disponíveis, as únicas opções são implementar a leitura retroativa manualmente com linguagens de programação como perl ou use:

grep -e "$pattern" file | tail -n1

Ou:

sed "/$pattern/h;$!d;g" file

Mas isso significa encontrar todas as correspondências e apenas imprimir a última.

    
por 04.02.2014 / 08:51
4

Aqui está uma solução possível que encontrará a localização da primeira ocorrência de padrão do último:

tac -s "$pattern" -r accounting.log | head -n 1

Isso faz uso das opções -s e -r de tac , que são as seguintes:

-s, --separator=STRING
use STRING as the separator instead of newline

-r, --regex
interpret the separator as a regular expression
    
por 02.02.2014 / 17:20
2

Usando sed

Mostrando alguns métodos alternativos para @responsa fina de Terdon usando sed :

$ sed '1!G;h;$!d' file | grep -m 1 $pattern
$ sed -n '1!G;h;$p' file | grep -m 1 $pattern

Exemplos

$ seq 10 > file

$ sed '1!G;h;$!d' file | grep -m 1 5
5

$ sed -n '1!G;h;$p' file | grep -m 1 5
5

Usando o Perl

Como bônus, aqui está uma notação mais fácil em Perl:

$ perl -e 'print reverse <>' file | grep -m 1 $pattern

Exemplo

$ perl -e 'print reverse <>' file | grep -m 1 5
5
    
por 02.02.2014 / 18:11