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

23

Eu tenho um arquivo com muitas linhas e cada linha tem um carimbo de data / hora no início, como

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

Portanto, frequentemente verifico duas coisas desse arquivo de log.

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

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

mtk
fonte
2
O que é condições globais e não head and tailfunciona para você?
margarida
Essa é a parte do meu arquivo de log. Eu estava tentando ser elaborador. Você pode ignorar isso.
mtk 21/09/12
Sua solução parece bem para mim. Se você quiser mais comodidade, transforme-a em uma função shell (até mesmo um apelido).
vonbrand
@vonbrand problema é que eu não seiN
Bernhard
@ Bernhard, eu não sou sed(1)especialista, mas existem maneiras de guardar coisas para uso posterior. Talvez valha a pena olhar lá dentro. OTOH, eu provavelmente criaria um script Perl (ou o que seja) para fazê-lo se usado com frequência, pois estou mais familiarizado com isso.
vonbrand

Respostas:

12

Você pode usar sedou awkfazer isso com um comando. No entanto, você perderá a velocidade, causará sede awkprecisará executar todo o arquivo de qualquer maneira. Do ponto de vista da velocidade, é muito melhor criar uma função ou sempre a combinação de tail+ head. Isso tem a desvantagem de não funcionar se a entrada for um canal, no entanto, você pode usar a substituição de processo, caso o shell o suporte (veja o exemplo abaixo).

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

e apenas iniciá-lo como

first_last "/path/to/file_to_process"

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

first_last <( command )

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

pressa
fonte
-n 10é o padrão, não?
L0b0
@ l0b0 sim, é o padrão. -n 10não é necessário aqui.
apressar
20

O @rush tem razão em usar head + tail, sendo mais eficiente para arquivos grandes, mas para arquivos pequenos (<20 linhas), algumas linhas podem ser exibidas duas vezes.

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

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

Stéphane Chazelas
fonte
Ao contrário da solução rushs, isso não funciona em um shell POSIX.
28513 Marco Marco
2
@Marco Huh? Somente construções POSIX são usadas aqui. O que você vê errado?
Gilles 'SO- stop be evil'
2
@ Gilles eu perdi o espaço: {head; tail;} < filefunciona no zsh, mas falha no sh. { head; tail;} < filesempre funciona. Desculpe pelo barulho.
28613 Marco
@Marco, se houvesse problemas com isso, seria com head, não com o shell. O POSIX precisa headdeixar o cursor no arquivo além dessas 10 linhas para arquivos regulares. Pode surgir um problema para headimplementações não POSIX (versões muito antigas do GNU head não eram conformes nesse caso, mas estamos falando de décadas) ou se o arquivo não for procurável (como pipe ou soquete nomeado, mas o outra solução teria o mesmo problema).
Stéphane Chazelas
1
@FCTW,sudo sh -c '{ head; tail;} < /path/to/file'
Stéphane Chazelas
9

A { head; tail; }solução não funcionaria em pipes (ou soquetes ou em outros arquivos não procuráveis) porque headpoderia consumir muitos dados conforme lidos por blocos e não pode procurar novamente em um pipe, potencialmente deixando o cursor dentro do arquivo além do que tailse entende selecionar.

Portanto, você pode usar uma ferramenta que lê um caractere de cada vez como o do shell read(aqui, usando uma função que usa o número de linhas principais e 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 implementar tailno awk, por exemplo, como:

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 tenha em atenção que algumas sedimplementações têm uma limitação baixa no tamanho do espaço do padrão, isso falharia com grandes valores do número de linhas finais).

Stéphane Chazelas
fonte
4

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

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

Observe que não é garantido que as linhas estejam em ordem, embora, para arquivos com mais de 8kB, provavelmente estejam. Esse ponto de corte de 8 kB é o tamanho típico do buffer de leitura e está relacionado ao motivo pelo qual | {head; tail;}não funciona em arquivos pequenos.

A cat >/dev/nullé necessária para manter o headgasoduto vivo. Caso contrário, teesairá mais cedo e, enquanto você obtiver saída tail, será de algum lugar no meio da entrada, e não no final.

Finalmente, por que, em >/dev/nullvez de, digamos, mudar tailpara outro |? No seguinte caso:

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

headO stdout é inserido no pipe, taile não no console, o que não é o que queremos.

Jander
fonte
Quando a cabeça ou a cauda terminam de escrever a saída desejada, eles fecham o stdin e saem. É daí que o SIGPIPE vem. Normalmente isso é uma coisa boa, eles estão descartando o restante da produção, portanto não há razão para o outro lado do tubo continuar gastando tempo gerando-o.
derobert 28/02
O que torna a ordem provavelmente confirmada? Provavelmente será para um arquivo grande, porque tailprecisa trabalhar mais, mas espero (e vejo) que falhe cerca da metade do tempo para entradas curtas.
Gilles 'SO- stop be evil'
Você obterá o SIGPIPE tee >(head) >(tail)pelos mesmos motivos ( >(...)que, a propósito, é um recurso ksh agora suportado pelo zsh e pelo bash) também usam pipes. Você poderia fazer isso, ... | (trap '' PIPE; tee >(head) >(tail) > /dev/null)mas ainda verá algumas mensagens de erro de tubos quebradostee .
Stéphane Chazelas
No meu sistema (bash 4.2.37, coreutils 8.13), tailestá sendo morto pelo SIGPIPE, não teee tailnão está gravando em um pipe. Então deve ser de um kill(), certo? E isso só acontece quando estou usando a |sintaxe. stracediz que teenão está ligando kill()... então talvez bash?
Jander
1
@Jander, tente alimentar mais do que como 8kseq 100000 | tee >(head -n1) >(tail -n1) > /dev/null
Stéphane Chazelas
3

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

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' 'H' '1,10p' '$-10,$p' 'q' | ed -s file
curx
fonte
Shorter:ed -s file <<< $'11,$-10d\n,p\nq\n'
don_crissti
2

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

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

Agora você pode fazer isso:

head_tail -n 5 < /path/to/file

É claro que isso pressupõe que você esteja visualizando apenas um arquivo e que a solução de Stephane funcione (de maneira confiável) apenas em arquivos regulares (que podem ser procurados).

l0b0
fonte
2

Com a opção -u( --unbuffered) do GNU sed, você pode usar sed -u 2qcomo uma alternativa sem buffer para head -n2:

$ seq 100|(sed -u 2q;tail -n2)
1
2
99
100

(head -n2;tail -n2)falha quando as últimas linhas fazem parte do bloco da entrada que é consumida por head:

$ seq 1000|(head -n2;tail -n2)
1
2
999
1000
$ seq 100|(head -n2;tail -n2)
1
2
nisetama
fonte
essa deve ser a melhor resposta! Funciona como um encanto!
Ben Usman
1

Encontrei algo assim hoje em dia, onde eu precisava apenas da última linha e algumas linhas da frente de um fluxo e criei 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, no EOF anexe a última linha ao espaço de espera, troque o espaço de espera e padrão e imprima o padrão espaço.

Talvez alguém com mais sed-fu do que eu posso imaginar como generalizar isso para imprimir as últimas poucas linhas do fluxo indicados nesta questão, mas eu não precisar e não poderia encontrar uma maneira fácil de fazer contas com base no $endereço no sedou talvez por gerir o espaço hold para que apenas as últimas linhas estão em quando EOFé atingido.

picos
fonte
1

Você pode experimentar o Perl, se o tiver instalado:

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

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

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 em pipes e com arquivos regulares.

Jasio
fonte