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.

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: 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.


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

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

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).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.