Obtendo texto do último marcador para EOF no POSIX.2

8

Eu tenho um texto com linhas de marcador como:

aaa
---
bbb
---
ccc

Preciso obter um texto do último marcador (não incluso) para o EOF. Nesse caso, será

ccc

Existe uma maneira elegante no POSIX.2? No momento, uso duas execuções: primeiro com nle greppara a última ocorrência com o respectivo número de linha. Então extraio o número da linha e uso sedpara extrair o pedaço em questão.

Os segmentos de texto podem ser muito grandes, então, tenho medo de usar algum método de adição de texto, como adicionamos o texto a um buffer, se encontrarmos o marcador, esvaziaremos o buffer, para que no EOF tenhamos nosso último pedaço no amortecedor.

aikipooh
fonte

Respostas:

6

A menos que seus segmentos sejam realmente grandes (como em: você realmente não pode poupar tanta memória RAM, provavelmente porque este é um pequeno sistema incorporado que controla um grande sistema de arquivos), uma única passagem é realmente a melhor abordagem. Não apenas porque será mais rápido, mas o mais importante, porque permite que a fonte seja um fluxo, a partir do qual os dados lidos e não salvos são perdidos. Este é realmente um trabalho para o awk, embora o sed também possa fazê-lo.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

Se você precisar usar uma abordagem de duas passagens, determine o deslocamento da linha do último separador e imprima a partir dele. Ou determine o deslocamento de bytes e imprima a partir dele.

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Adendo: Se você possui mais do que o POSIX, aqui está uma versão simples de uma passagem que se baseia em uma extensão comum do awk que permite que o separador de registros RSseja uma expressão regular (o POSIX permite apenas um único caractere). Não está completamente correto: se o arquivo terminar com um separador de registros, ele imprimirá o pedaço antes do último separador de registros, em vez de um registro vazio. A segunda versão RTevita esse defeito, mas RTé específica do GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
Gilles 'SO- parar de ser mau'
fonte
@ Gilles: sedestá funcionando bem, mas não consigo awkexecutar o exemplo; trava ... e recebo um erro no terceiro exemplo: cut -f ':' -t 1 ... cut: invalid option - 't'
Peter.O
@ fred.bear: Não faço ideia de como isso aconteceu - testei todos os meus trechos, mas de alguma forma errei a edição pós-copiar-colar no cutexemplo. Não vejo nada de errado com o awkexemplo, qual versão do awk você está usando e qual é a sua entrada de teste?
Gilles 'SO- stop be evil'
... na verdade, a awkversão está funcionando .. está demorando muito tempo em um arquivo grande .. a sedversão processou o mesmo arquivo em 0.470s .. Meus dados de teste são muito ponderados ... apenas dois pedaços com um único '---' três linhas a partir do final de 1 milhão de linhas ...
Peter.O
@ Gilles .. (acho que devo parar de testar às 3 da manhã. De alguma forma, testei os três awks de "dois passes" como uma única unidade :( ... agora testei cada um individualmente e o segundo é muito rápido em 0,204 segundos ... Howerver, apenas o primeiro awk de duas passagens produz apenas: " (entrada padrão) " (o -l parece ser o culpado) ... quanto ao terceiro awk de duas passagens, eu não não produz nada ... mas o segundo "two-pass" é o mais rápido de todos os métodos apresentados (POSIX ou não
:)
@ fred.bear: fixo e fixo. Meu controle de qualidade não é muito bom para esses trechos curtos - normalmente copio e colo de uma linha de comando, formato, aviso um bug e tento consertar em linha, em vez de reformatar. Estou curioso para ver se a contagem de caracteres é mais eficiente do que a contagem de linhas (métodos de 2ª e 3ª passagem).
Gilles 'SO- stop being evil' em
3

Uma estratégia de dois passes parece ser a coisa certa. Em vez de sed, eu usaria awk(1). Os dois passes podem ficar assim:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

para obter o número da linha. E, em seguida, faça eco de todo o texto a partir desse número de linha com:

$ awk "NR>$LINE" file

Isso não deve exigir armazenamento em buffer excessivo.

Mackie Messer
fonte
e então eles podem ser combinados:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
glenn jackman 31/03
Vendo que estive testando as outras inscrições, agora também testei o "glen jackman's" acima do trecho. Demora 0,352 segundos (com o mesmo arquivo de dados mencionado na minha resposta) ... Estou começando a receber a mensagem de que o awk pode ser mais rápido do que eu pensava ser possível (achei que o sed era o melhor que podia, mas parece ser um caso de "cavalos para cursos") ...
Peter.O
Muito interessante ver todos esses scripts comparados. Bom trabalho, Fred.
Mackie Messer
As soluções mais rápidas usam tac e tail, que realmente lêem o arquivo de entrada ao contrário. Agora, se apenas awk poderia ler os para trás de arquivo de entrada ...
Mackie Messer
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

O primeiro sedgera números de linha das linhas "---" ...
O segundo sedextrai o último número da saída do primeiro sed ...
Adicione 1 a esse número para iniciar o bloco "ccc" ...
O terceiro saídas 'sed' desde o início do bloco "ccc" para o EOF

Atualização (com informações recomendadas sobre os métodos Gilles)

Bem, eu estava pensando sobre o desempenho de Glenn Jackman tac, então testei as três respostas (no momento da redação) ... Os arquivos de teste continham 1 milhão de linhas (de seus próprios números de linhas).
Todas as respostas fizeram o que era esperado ...

Aqui estão os tempos ..


Gilles sed (passe único)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Gilles awk (passe único)

# very slow, but my data had a very large data block which awk needed to cache.

Gilles 'two-pass' (primeiro método)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Gilles 'two-pass' (segundo método) ... muito rápido

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Gilles 'two-pass' (terceiro método)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Gilles 'gawk' (método RT) ... muito rápido , mas não é POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

glenn jackman ... muito rápido , mas não é POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Mackie Messer

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s
Peter.O
fonte
Por curiosidade, qual das minhas versões de duas passagens você testou e qual versão do awk você usou?
Gilles 'SO- stop be evil'
@Gilles: Eu usei o GNU Awk 3.1.6 (no Ubuntu 10.04 com 4 GB de RAM). Todos os testes têm 1 milhão de linhas no primeiro "bloco", depois um "marcador" seguido de 2 linhas "de dados" ... Demorou 15.540 segundos para processar um arquivo menor de 100.000 linhas, mas para as 1.000.000 linhas, estou executando-o agora e já faz mais de 25 minutos. Ele está usando um núcleo para 100% ... matando-o agora ... Aqui estão mais alguns testes incrementais: linhas = 100000 (0m16.026s) - linhas = 200000 (2m29.990s) - linhas = 300000 (5m23). 393s) - lines = 400000 (11m9.938s)
Peter.O
Ops .. No meu comentário acima, eu perdi sua referência awk de "duas passagens". O detalhe acima é para o awk de "passagem única" ... A versão do awk está correta ... Fiz mais comentários sobre as diferentes versões de "duas passagens" na sua resposta (modifiquei os resultados de tempo acima)
Pedro.O
2

Use " tac ", que exibe as linhas de um arquivo do começo ao fim:

tac afile | awk '/---/ {exit} {print}' | tac
Glenn Jackman
fonte
tacnão é POSIX, é específico do Linux (está no GNU coreutils e em algumas instalações do busybox).
Gilles 'SO- stop be evil'
0

Você poderia apenas usar ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

Como funciona: tduplica a .linha atual ( ) - que é sempre a última linha quando é ediniciada (apenas no caso de o delimitador estar presente na última linha), 1,?===?dexclui todas as linhas até e incluindo a correspondência anterior ( edainda está na última linha) ) $dexclui a última linha (duplicada), ,pimprime o buffer de texto (substitua por wpara editar o arquivo no local) e finalmente qfecha ed.


Se você sabe que há pelo menos um delimitador na entrada (e não se importa se também é impresso),

sed 'H;/===/h;$!d;x' infile

seria o mais curto.
Como funciona: anexa todas as linhas ao Hbuffer antigo, substitui o hbuffer antigo ao encontrar uma correspondência, dexclui todas as linhas, exceto a última $quando xaltera os buffers (e as impressões automáticas).

don_crissti
fonte