Comando Awk para imprimir todas as linhas, exceto as últimas três linhas

5

Eu quero imprimir todas as linhas, exceto as últimas três linhas da entrada apenas através do awk. Por favor, note que meu arquivo contém n linhas.

Por exemplo,

file.txt contém,

foo
bar
foobar
barfoo
last
line

Eu quero que a saída seja

foo
bar
foobar

Eu sei que isso seria possível com a combinação de tac e sed ou tac e awk

$ tac file | sed '1,3d' | tac
foo
bar
foobar

$ tac file | awk 'NR==1{next}NR==2{next}NR==3{next}1' | tac
foo
bar
foobar

Mas eu quero a saída apenas pelo awk.

    
por Avinash Raj 02.06.2014 / 13:28

3 respostas

15

É sempre tão desajeitado, mas você pode adicionar todas as linhas a uma matriz e, no final, quando souber a duração, exibir tudo, exceto as últimas três linhas.

... | awk '{l[NR] = $0} END {for (i=1; i<=NR-3; i++) print l[i]}'

Outra abordagem (mais eficiente aqui ) é o empilhamento manual em três variáveis:

... | awk '{if (a) print a; a=b; b=c; c=$0}'

a só imprime depois que uma linha foi movida de c para b e, em seguida, para a , portanto, isso limita a três linhas. O upsides imediato é que ele não armazena todo o conteúdo na memória e não deve causar problemas de buffer ( fflush() após a impressão se isso acontecer), mas a desvantagem aqui é que não é simples escalar isso. Se você quiser pular as últimas 100 linhas, você precisa de 100 variáveis e 100 variáveis juggles.

Se o awk tivesse operadores push e pop para matrizes, seria mais fácil.

Ou podemos pré-calcular o número de linhas e até onde queremos ir com $(($(wc -l < file) - 3)) . Isso é relativamente inútil para o conteúdo transmitido , mas em um arquivo, funciona muito bem:

awk -v n=$(($(wc -l < file) - 3)) 'NR<n' file

Geralmente, você só usa head :

$ seq 6 | head -n-3
1
2
3

Usando o benchmark de terdon , podemos ver como eles se comparam. Eu pensei em oferecer uma comparação completa:

  • head : 0.018s (me)
  • awk + wc : 0.169s (eu)
  • awk 3 variáveis: 0.178s (me)
  • awk double-file: 0.322s (terdon)
  • awk buffer circular: 0.355s (Examinador)
  • awk for-loop: 0.693s (eu)

A solução mais rápida é usar um utilitário otimizado para C, como head ou wc , lidar com coisas pesadas, mas em puro awk , a pilha de rotação manual é a melhor opção por enquanto. / p>     

por Oli 02.06.2014 / 13:43
5

Para uso mínimo de memória, você pode usar um buffer circular:

awk 'NR>n{print A[NR%n]} {A[NR%n]=$0}' n=3 file

Usando o operador mod nos números de linha, temos no máximo n entradas de matriz.

Tomando o exemplo de n = 3:

Na linha 1 NR%n é igual a 1, a linha 2 produz 2 e a linha 3 produz 0 e a linha 4 é avaliada como 1 novamente.

Line 1 -> A[1]
Line 2 -> A[2]
Line 3 -> A[0]
Line 4 -> A[1]
Line 5 -> A[2]
...

Quando chegamos à linha 4, A[NR%n] contém o conteúdo da linha 1. Então, isso é impresso e A[NR%n] obtém o conteúdo da linha 4. A próxima linha (linha 5) do conteúdo original da linha 2 é impressa e assim por diante, até chegarmos ao fim. O que permanece não impresso é o conteúdo do buffer, que contém as últimas 3 linhas ...

    
por Scrutinizer 02.06.2014 / 14:28
2

Você também pode processar o arquivo duas vezes para evitar qualquer coisa na memória:

awk '{if(NR==FNR){c++}else if(FNR<=c-3){print}}' file file

O truque aqui é o NR==FNR test. NR é o número da linha atual e FNR é o número da linha atual do arquivo atual. Se mais de um arquivo for passado como entrada, FNR será igual a NR apenas enquanto o primeiro arquivo estiver sendo processado. Dessa forma, obtemos rapidamente o número de linhas no primeiro arquivo e salvamos como c . Como os "dois" arquivos são na verdade os mesmos, agora sabemos o número de linhas que queremos, então só imprimiremos se for um deles.

Embora você possa pensar que isso será mais lento do que as outras abordagens, é realmente mais rápido, já que não há nenhum processamento em andamento. Tudo é feito usando as ferramentas awk internas ( NR e FNR ) além de uma única comparação aritmética. Eu testei em um arquivo de 50MB com um milhão de linhas criadas com este comando:

for i in {500000..1000000}; do 
    echo "The quick brown fox jumped over the lazy dog $i" >> file; 
done

Como você pode ver, os tempos são quase idênticos, mas a abordagem que forneci aqui é ligeiramente mais rápida que a primeira sugestão de Oli (mas mais lenta que as outras):

$ for i in {1..10}; do ( 
    time awk '{if(NR==FNR){c++}else if(FNR<=c-3){print}}' file file > /dev/null ) 2>&1 | 
       grep -oP 'real.*?m\K[\d\.]+'; 
  done | awk '{k+=$1}END{print k/10" seconds"}'; 
0.4757 seconds

$  for i in {1..10}; do ( 
    time awk '{l[NR] = $0} END {for (i=1; i<=NR-3; i++) print l[i]}' file > /dev/null ) 2>&1 | 
        grep -oP 'real.*?m\K[\d\.]+'; 
   done | awk '{k+=$1}END{print k/10" seconds"}'; 
0.5347 seconds
    
por terdon 02.06.2014 / 18:13