Comando para exibir as primeiras e últimas linhas de um arquivo

23

Eu tenho um arquivo com muitas linhas, e cada linha tem um timestamp no início, como

[Thread-3] (21/09/12 06:17:38:672) logged message from code.....

Então, eu freqüentemente verifico 2 coisas deste arquivo de log.

  1. As primeiras linhas, que têm as condições globais e a hora de início, também são fornecidas.
  2. Últimas poucas linhas, que têm o status de saída com outras informações.

Existe algum comando simples e rápido que permita mostrar apenas as primeiras e últimas linhas de um arquivo?

    
por mtk 21.09.2012 / 11:22

9 respostas

12

Você pode usar sed ou awk para fazer isso com um comando. No entanto, você perderá velocidade, pois sed e awk precisarão percorrer todo o arquivo mesmo assim. Do ponto de vista da velocidade, é muito melhor fazer uma função ou, a cada vez, a combinação de tail + head . Isto tem o lado negativo de não funcionar se a entrada for um pipe, no entanto você pode usar a substituição do processo, caso seu shell o suporte (veja o exemplo abaixo).

first_last () {
    head -n 10 -- "$1"
    tail -n 10 -- "$1"
}

e inicie-o como

first_last "/path/to/file_to_process"

para prosseguir com a substituição do processo (bash, zsh, ksh apenas como shells):

first_last <( command )

ps. você pode até adicionar um grep para verificar se suas "condições globais" existem.

    
por 21.09.2012 / 11:44
20

@rush está certo sobre o uso do head + tail ser mais eficiente para arquivos grandes, mas para arquivos pequenos (< 20 linhas), algumas linhas podem ser produzidas duas vezes.

{ head; tail;} < /path/to/file

seria igualmente eficiente, mas não teria o problema acima.

    
por 21.09.2012 / 14:02
8

A solução { head; tail; } não funcionaria em pipes (ou soquetes ou qualquer outro arquivo não procurado) porque head poderia consumir muitos dados à medida que fosse lida por blocos e não puder voltar atrás em um tubo o cursor dentro do arquivo além do que tail deve selecionar.

Assim, você poderia usar uma ferramenta que lê um caractere de cada vez, como o read do shell (usando uma função que leva o número de linhas de cabeçalho e linhas finais como argumentos).

head_tail() {
  n=0
  while [ "$n" -lt "$1" ]; do
    IFS= read -r line || { printf %s "$line"; break; }
    printf '%s\n' "$line"
    n=$(($n + 1))
  done
  tail -n "${2-$1}"
}
seq 100 | head_tail 5 10
seq 20 | head_tail 5

ou implemente tail no awk, por exemplo:

head_tail() {
  awk -v h="$1" -v t="${2-$1}" '
    {l[NR%t]=$0}
    NR<=h
    END{
      n=NR-t+1
      if(n <= h) n = h+1
      for (;n<=NR;n++) print l[n%t]
    }'
}

com sed :

head_tail() {
  sed -e "1,${1}b" -e :1 -e "$(($1+${2-$1})),\$!{N;b1" -e '}' -e 'N;D'
}

(embora esteja ciente de que algumas implementações de sed têm uma limitação baixa no tamanho de seu espaço de padrão, assim falharíamos para grandes valores do número de linhas finais).

    
por 28.02.2013 / 17:01
4

Usando a substituição do processo bash , você pode fazer o seguinte:

make_some_output | tee >(tail -n 2) >(head -n 2; cat >/dev/null) >/dev/null

Note que não é garantido que as linhas estejam em ordem, apesar de que, para arquivos maiores que 8kB, elas provavelmente estarão. Esse limite de 8kB é o tamanho típico do buffer de leitura e está relacionado ao motivo | {head; tail;} não funcionar para arquivos pequenos.

O cat >/dev/null é necessário para manter o head pipeline ativo. Caso contrário, tee será encerrado antecipadamente e, embora você receba a saída de tail , será de algum lugar no meio da entrada, e não no final.

Por fim, por que o >/dev/null em vez de, digamos, mover tail para outro | ? No seguinte caso:

make_some_output | tee >(head -n 2; cat >/dev/null) | tail -n 2  # doesn't work
O stdout de

head é alimentado no pipe para tail em vez do console, o que não é o que queremos de todo.

    
por 28.02.2013 / 17:07
3

Usando ed (que lerá todo o arquivo na RAM):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' 'H' '1,10p' '$-10,$p' 'q' | ed -s file
    
por 01.03.2013 / 17:17
2

A primeira solução de Stephane em uma função para que você possa usar argumentos (funciona em qualquer shell POSIX ou Bourne):

head_tail() {
    head "$@";
    tail "$@";
}

Agora você pode fazer isso:

head_tail -n 5 < /path/to/file

Isto, obviamente, pressupõe que você esteja olhando apenas um arquivo e que a solução de Stephane funcione (confiavelmente) somente em arquivos regulares (procuráveis).

    
por 28.02.2013 / 17:21
2

(head;tail) não funciona em um pipeline quando as linhas que devem ser impressas por tail são consumidas por head :

$ seq 100|(head -n2;tail -n2)
1
2
$ seq 1000|(head -n2;tail -n2)
1
2
999
1000

Com o GNU sed , você pode usar sed -u 2q como uma alternativa sem buffer para head -n2 :

$ seq 100|(sed -u 2q;tail -n2)
1
2
99
100
    
por 25.04.2015 / 21:48
1

Eu encontrei algo assim hoje, onde precisei apenas da última linha e de algumas linhas da frente de um stream e consegui o seguinte.

sed -n -e '1{h}' -e '2,3{H}' -e '${H;x;p}'

Eu li isso como: inicialize o espaço de espera com o conteúdo da primeira linha, acrescente as linhas 2-3 no espaço de espera, em EOF, anexe a última linha ao espaço de espera, troque espaço de espera e padrão e imprima o espaço padrão.

Talvez alguém com mais sed -fu do que eu possa descobrir como generalizar isso para imprimir as últimas poucas linhas do fluxo indicado nesta pergunta, mas eu não precisei dele e Não foi possível encontrar uma maneira fácil de fazer cálculos com base no endereço $ em sed ou talvez gerenciando o espaço de armazenamento para que apenas as últimas linhas estejam nele quando EOF for atingido.

    
por 14.03.2014 / 08:31
1

Você pode experimentar o Perl, se tiver instalado:

perl -e '@_ = <>; @_=@_[0, -3..-1]; print @_'

Isso funcionará para a maioria dos arquivos, mas lê todo o arquivo na memória antes de processá-lo. Se você não estiver familiarizado com fatias de Perl, "0" entre colchetes significa "pegue a primeira linha" e "-3 ... - 1" significa "pegue as últimas três linhas". Você pode adaptar os dois às suas necessidades. Se você precisar processar arquivos realmente grandes (o que é 'grande' pode depender da sua RAM e talvez tamanhos de swap), você pode querer:

perl -e 'while($_=<>){@_=(@_,$_)[0,-3..-1]}; print @_'

pode ser um pouco mais lento, porque faz uma fatia a cada iteração, mas é independente do tamanho do arquivo.

Ambos os comandos devem funcionar tanto em pipes quanto em arquivos regulares.

    
por 22.11.2013 / 19:34