Obtendo o texto do último marcador para EOF em POSIX.2

8

Eu tenho um texto com linhas de marcadores como:

aaa
---
bbb
---
ccc

Eu preciso de um texto do último marcador (não inclusivo) para EOF. Neste caso, será

ccc

Existe uma maneira elegante no POSIX.2? Agora eu uso duas execuções: primeiro com nl e grep para a última ocorrência com o respectivo número de linha. Então eu extraio o número da linha e uso sed para extrair o pedaço em questão.

Os segmentos de texto podem ser bem grandes, então eu tenho medo de usar algum método de adição de texto como nós adicionamos o texto a um buffer, se encontrarmos o marcador nós esvaziamos o buffer, então na EOF nós temos nosso último pedaço no buffer.

    
por aikipooh 30.03.2011 / 15:52

5 respostas

6

A menos que seus segmentos sejam realmente grandes (como em: você realmente não pode poupar muita RAM, presumivelmente porque este é um pequeno sistema embarcado 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 qualquer dado lido e não salvo é perdido. Este é realmente um trabalho para o awk, embora o sed também possa fazer isso.

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 dois passos, determine o deslocamento de linha do último separador e imprima a partir dele. Ou determine o deslocamento de bytes e imprima a partir disso.

</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ê tem mais que POSIX, aqui está uma versão simples de um passo que depende de uma extensão comum ao awk que permite que o separador de registro RS seja uma expressão regular (POSIX só permite um único caractere). Não está completamente correto: se o arquivo terminar com um separador de registro, ele imprimirá o fragmento antes do último separador de registro em vez de um registro vazio. A segunda versão usando RT evita esse defeito, mas RT é específico do GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
    
por 31.03.2011 / 20:52
3

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

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

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

$ awk "NR>$LINE" file

Isso não deve exigir buffer excessivo.

    
por 31.03.2011 / 12:03
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

O primeiro sed exibe números de linha das linhas "---" ...
O segundo sed extrai o último número da saída do primeiro sed ...
Adicione 1 a esse número para obter o início do seu bloco "ccc" ...
O terceiro 'sed' sai do início do bloco "ccc" para EOF

Atualizar (com informações atualizadas sobre os métodos de Gilles)

Bem, eu estava me perguntando sobre como o glenn jackman tac iria se apresentar, então testei no tempo as três respostas (no momento da escrita) ... O (s) arquivo (s) de teste continha 1 milhão de linhas (de seus próprios números de linha). Todas as respostas fizeram o que era esperado ...

Aqui estão os tempos ...

Gilles sed (passagem única)

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

Gilles awk (passagem única)

# 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 31.03.2011 / 19:56
2

Use " tac " que gera as linhas de um arquivo do final ao início:

tac afile | awk '/---/ {exit} {print}' | tac
    
por 31.03.2011 / 18:46
0

Você poderia usar apenas ed

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

Como funciona: t duplica a linha atual ( . ) - que é sempre a última linha quando ed inicia (caso o delimitador esteja presente na última linha), 1,?===?d exclui todos alinha até e inclusive a correspondência anterior ( ed ainda está na última linha), então $d exclui a última linha (duplicada), ,p imprime o buffer de texto (substitua por w para editar o arquivo no lugar ) e finalmente q sai ed .

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

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

seria o menor.
Como funciona: ele anexa todas as linhas ao buffer H old, sobrescreve o buffer h old ao encontrar uma correspondência, ele d elimina todas as linhas, exceto a $ t quando e x altera buffers (e autoprints).

    
por 09.09.2018 / 14:59