Ottenere testo dall'ultimo marcatore a EOF in POSIX.2


8

Ho un testo con linee di marcatura come:

aaa
---
bbb
---
ccc

Ho bisogno di ottenere un testo dall'ultimo marcatore (non incluso) a EOF. In questo caso lo sarà

ccc

Esiste un modo elegante in POSIX.2? In questo momento uso due esecuzioni: prima con nle grepper l'ultima occorrenza con il rispettivo numero di riga. Quindi estraggo il numero di riga e utilizzo sedper estrarre il blocco in questione.

I segmenti di testo possono essere piuttosto grandi, quindi ho paura di usare un metodo di aggiunta del testo come se aggiungessimo il testo a un buffer, se incontriamo il marker svuotiamo il buffer, in modo che su EOF abbiamo il nostro ultimo pezzo nel buffer.

Risposte:


6

A meno che i tuoi segmenti non siano davvero enormi (come in: non puoi davvero risparmiare così tanta RAM, presumibilmente perché si tratta di un piccolo sistema incorporato che controlla un file system di grandi dimensioni), un singolo passaggio è davvero l'approccio migliore. Non solo perché sarà più veloce, ma soprattutto perché consente alla sorgente di essere un flusso, dal quale vengono persi tutti i dati letti e non salvati. Questo è davvero un lavoro per Awk, anche se sed può farlo.

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 è necessario utilizzare un approccio a due passaggi, determinare l'offset della linea dell'ultimo separatore e stampare da quello. Oppure determina l'offset del byte e stampa da quello.

</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)
')

Addendum: se hai più di POSIX, ecco una semplice versione one-pass che si basa su un'estensione comune di awk che consente al separatore di record RSdi essere un'espressione regolare (POSIX consente solo un singolo carattere). Non è completamente corretto: se il file termina con un separatore di record, stampa il blocco prima dell'ultimo separatore di record anziché un record vuoto. La seconda versione che usa RTevita quel difetto, ma RTè specifica per GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'

@Gilles: sedfunziona bene, ma non riesco a far funzionare l' awkesempio; si blocca ... e viene visualizzato un errore nel terzo esempio: cut -f ':' -t 1 ... cut: opzione non valida - 't'
Peter.O

@ fred.bear: non ho idea di come sia successo: ho testato tutti i miei frammenti, ma in qualche modo ho incasinato la modifica post-copia-incolla cutsull'esempio. Non vedo nulla di sbagliato awknell'esempio, quale versione di awk stai usando e qual è il tuo input di test?
Gilles 'SO- smetti di essere malvagio'

... in realtà la awkversione funziona ... ci vuole molto tempo su un file di grandi dimensioni .. la sedversione ha elaborato lo stesso file in 0.470s .. I miei dati di test sono molto ponderati ... solo due blocchi con un solo "---" tre righe dalla fine di 1 milione di righe ...
Peter

@Gilles .. (Penso che dovrei smettere di testare alle 3 del mattino. In qualche modo ho testato tutti e tre i awk "a due passaggi" come una singola unità :( ... Ora ho testato ciascuno singolarmente e il secondo è molto veloce a 0.204 secondi ... Howerver, solo il primo awk "a due passaggi": " (input standard) " (il -l sembra essere il colpevole) ... come per il terzo awk a "due passaggi", io no non emette nulla ... ma il secondo "due passaggi" è il più veloce di tutti i metodi presentati (POSIX o altro
:)

@ fred.bear: fisso e fisso. Il mio QA non è molto buono per questi brevi frammenti: in genere copio e incollo da una riga di comando, formatto, quindi noto un bug e provo a correggere inline piuttosto che a riformattare. Sono curioso di vedere se il conteggio dei caratteri è più efficiente del conteggio delle linee (metodi 2a contro 3a a due passaggi).
Gilles 'SO- smetti di essere malvagio'

3

Una strategia a due passaggi sembra essere la cosa giusta. Invece di sed vorrei usare awk(1). I due passaggi potrebbero apparire così:

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

per ottenere il numero di riga. E poi fai eco a tutto il testo a partire da quel numero di riga con:

$ awk "NR>$LINE" file

Ciò non dovrebbe richiedere un buffering eccessivo.


e quindi possono essere combinati:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
glenn jackman,

Visto che ho testato gli altri invii, ora ho anche testato lo snippet "glen jackman's". Ci vogliono 0,352 secondi (con lo stesso file di dati menzionato nella mia risposta) ... Sto iniziando a ricevere il messaggio che awk può essere più veloce di quanto inizialmente credessi possibile (pensavo che sed fosse buono come quello che era, ma sembra essere un caso di "cavalli per corsi") ...
Peter.O

Molto interessante vedere tutti questi script messi a confronto. Bel lavoro Fred.
Mackie Messer

Le soluzioni più veloci utilizzano tac e tail che in realtà leggono il file di input all'indietro. Ora, se solo Awk potesse leggere il file di input all'indietro ...
Mackie Messer

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

I primi sednumeri di riga di output delle righe "---" ...
Il secondo sedestrae l'ultimo numero dall'output del primo sed ...
Aggiungi 1 a quel numero per ottenere l'inizio del blocco "ccc" ...
Il terzo 'sed' emette dall'inizio del blocco "ccc" su EOF

Aggiornamento (con informazioni aggiornate sui metodi Gilles)

Beh, mi chiedevo come si tac sarebbe comportato Glenn Jackman , quindi ho testato nel tempo le tre risposte (al momento della stesura) ... I file di test contenevano ciascuno 1 milione di righe (dei loro numeri di riga).
Tutte le risposte hanno fatto come previsto ...

Ecco i tempi ..


Gilles sed (passaggio singolo)

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

Gilles awk (passaggio singolo)

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

Gilles 'a due passaggi' (primo metodo)

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

Gilles 'a due passaggi' (secondo metodo) ... molto veloce

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

Gilles 'a due passaggi' (terzo metodo)

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

Gilles 'gawk' (metodo RT) ... molto veloce , ma non è POSIX.

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

glenn jackman ... molto veloce , ma non è 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

Per curiosità, quale delle mie versioni a due passaggi hai testato e quale versione di awk hai usato?
Gilles 'SO-smetti di essere malvagio' il

@Gilles: ho usato GNU Awk 3.1.6 (in Ubuntu 10.04 con 4 GB di RAM). Tutti i test hanno 1 milione di righe nel primo "blocco", quindi un "marker" seguito da 2 righe "dati" ... Sono stati necessari 15.540 secondi per elaborare un file più piccolo di 100.000 righe, ma per le 1.000.000 di righe, sono eseguendolo ora, ed è stato finora più di 25 minuti. Sta usando un core al 100% ... uccidendolo ora ... Ecco alcuni altri test incrementali: righe = 100000 (0m16.026s) - righe = 200000 (2m29.990s) - righe = 300000 (5m23. 393s) - lines = 400000 (11m9.938s)
Peter.O

Oops .. Nel mio commento sopra, ho perso il tuo riferimento awk "a due passaggi". Il dettaglio sopra è per il awk "single-pass" ... La versione awk è corretta ... Ho fatto ulteriori commenti riguardo alle diverse versioni "a due passaggi" sotto la tua risposta (modificato i risultati di tempo sopra)
Peter.O


0

Potresti semplicemente usare ed

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

Come funziona: tduplica la .riga corrente ( ) - che è sempre l'ultima riga all'avvio ed(nel caso in cui il delimitatore sia presente sull'ultima riga), 1,?===?delimina tutte le righe fino alla riga precedente inclusa ( edè ancora sull'ultima riga) ) quindi $delimina l'ultima riga (duplicata), ,pstampa il buffer di testo (sostituirlo con wper modificare il file in posizione) e infine si qchiude ed.


Se sai che c'è almeno un delimitatore nell'input (e non importa se è anche stampato), allora

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

sarebbe il più corto.
Come funziona: aggiunge tutte le righe al Hvecchio buffer, sovrascrive il hvecchio buffer quando incontra una corrispondenza, delimina tutte le righe tranne $quella quando xcambia buffer (e stampa automatica).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.