Grep do final de um arquivo até o início

38

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

O comando:

tac accounting.log | grep $pattern

dá o que eu preciso, mas é muito lento porque o sistema operacional precisa primeiro ler o arquivo inteiro e depois enviar para o canal.

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

Hábner Costa
fonte

Respostas:

44

tacsó ajuda se você também usar grep -m 1(supondo que o GNU grep) greppare após a primeira partida:

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, ambos tace grepprecisam processar o arquivo inteiro, portanto, o uso tacé meio inútil.

Portanto, a menos que você use grep -m, não use tacnada, basta analisar a saída de greppara obter a última correspondência:

grep foo accounting.log | tail -n 1 

Outra abordagem seria usar 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
terdon
fonte
11
Estou usando o tac porque preciso encontrar a última correspondência de um determinado padrão. Usando sua sugestão "grep -m1", o tempo de execução varia de 0m0.597s a 0m0.007s \ o /. Obrigado a todos!
Hábner Costa 02/02
11
@ HábnerCosta, de nada. Entendo por que você está usando tac, o que quero dizer é que isso não ajuda, a menos que você também use, -mpois o arquivo ainda precisa ser lido na íntegra por dois programas. Caso contrário, você poderia procurar todas as ocorrências e manter apenas a última, como eu faço tail -n 1.
terdon
6
Por que você diz "tac [...] precisa processar o arquivo inteiro"? A primeira coisa que o tac faz é procurar o final do arquivo e ler um bloco do final. Você pode verificar isso sozinho com strace (1). Quando combinado com grep -m, deve ser bastante eficiente.
Camh
11
@camh quando combinado com grep -mele. O OP não estava usando, -mportanto, grep e tac estavam processando a coisa toda.
terdon
Você poderia expandir o significado da awklinha?
Sopalajo de Arrierez
12

A razão porque

tac file | grep foo | head -n 1

não para na primeira partida é por causa do buffer.

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

Mas o que acontece é que, como sua saída não está indo para um terminal, grepele é armazenado em buffer. Ou seja, ele não está gravando até que tenha acumulado o suficiente (4096 bytes no meu teste com o GNU grep).

O que isso significa é que grepnão será encerrado antes de gravar 8192 bytes de dados, portanto, provavelmente algumas linhas.

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

Mas com o GNU de grepqualquer maneira, você pode usar -m 1como o @terdon mostrou, o que é melhor quando ele sai na primeira partida.

Se você grepnão é o GNU grep, então você pode usar sedou awkpreferir. Mas, tac sendo um comando GNU, duvido que você encontre um sistema em tacque grepnão seja o GNU grep.

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

Alguns sistemas precisam tail -rfazer a mesma coisa que o GNU tac.

Observe que, para arquivos regulares (que podem ser procurados), tace tail -rsão eficientes porque eles lêem os arquivos para trás, eles não estão apenas lendo o arquivo completamente na memória antes de imprimi-lo para trás (como faria a abordagem sed do @ slm ou tacem arquivos não regulares) .

Em sistemas onde tacnem tail -rexistem nem estão disponíveis, as únicas opções são implementar a leitura reversa manualmente com linguagens de programação como perlou usar:

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

Ou:

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

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

Stéphane Chazelas
fonte
4

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

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

Isso faz uso dos comutadores -se -rdos tacquais são os seguintes:

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

-r, --regex
interpret the separator as a regular expression
mkc
fonte
Exceto que você perderá tudo o que estiver entre o início da linha e o padrão.
ychaouche
2

Usando sed

Mostrando alguns métodos alternativos para a boa resposta 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 Perl

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

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

Exemplo

$ perl -e 'print reverse <>' file | grep -m 1 5
5
slm
fonte
11
É sedprovável que essa seja (especialmente a ) várias ordens de magnitude mais lentas que grep 5 | tail -n1ou sed '/5/h;$!d;g'. Também usará potencialmente muita memória. Não é muito mais portátil, pois você ainda está usando o GNU grep -m.
Stéphane Chazelas