Come grep-inverse-match ed escludere le righe "prima" e "dopo"


26

Prendi in considerazione un file di testo con le seguenti voci:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Dato un modello (ad esempio fff), vorrei grep il file sopra per ottenere l'output:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

Ad esempio, se B = 2e A = 1, l'output con pattern = fffdovrebbe essere:

aaa
bbb
ccc
hhh
iii

Come posso farlo con grep o altri strumenti da riga di comando?


Nota, quando provo:

grep -v 'fff'  -A1 -B2 file.txt

Non capisco quello che voglio. Invece ottengo:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii

Risposte:


9

don potrebbe essere migliore nella maggior parte dei casi, ma nel caso in cui il file sia davvero grande e non riesci seda gestire un file di script così grande (che può accadere a circa 5000+ righe di script) , eccolo qui sed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Questo è un esempio di quella che viene chiamata una finestra scorrevole sull'input. Funziona costruendo un buffer look-$B forward di -count prima di tentare di stampare qualsiasi cosa.

E in realtà, probabilmente dovrei chiarire il mio punto precedente: il principale limitatore di prestazioni sia per questa soluzione che per quella di don sarà direttamente correlato all'intervallo. Questa soluzione rallenterà con intervalli di dimensioni maggiori , mentre quella di don rallenterà con frequenze di intervallo maggiori . In altre parole, anche se il file di input è molto grande, se il verificarsi dell'intervallo effettivo è ancora molto raro, la sua soluzione è probabilmente la strada da percorrere. Tuttavia, se la dimensione dell'intervallo è relativamente gestibile ed è probabile che si verifichi spesso, questa è la soluzione che dovresti scegliere.

Quindi ecco il flusso di lavoro:

  • Se $matchsi trova nello spazio del modello preceduto da una \newline, elimina sedricorsivamente Dogni \newline che la precede.
    • $matchPrima stavo eliminando completamente lo spazio del modello, ma per gestire facilmente la sovrapposizione, lasciare un punto di riferimento sembra funzionare molto meglio.
    • Ho anche cercato s/.*\n.*\($match\)/\1/di provare a farlo in una volta sola e schivare il loop, ma quando $A/$Bsono grandi, il Dloop elete si rivela notevolmente più veloce.
  • Quindi inseriamo la Nriga ext di input preceduta da un \ndelimitatore di ewline e proviamo ancora una volta a Deliminare /\n.*$match/una ancora facendo riferimento alla nostra espressione regolare usata più di recente w / //.
  • Se lo spazio del motivo corrisponde, $matchallora può farlo solo con $matchall'inizio della linea - tutte le $Blinee precedenti sono state cancellate.
    • Quindi iniziamo a fare un giro su $After.
    • Ogni esecuzione di questo ciclo cercheremo di s///ubstitute per &$Al'esimo \ncarattere ewline nello spazio modello, e, in caso di successo, tEst saranno noi ramo - e tutta la nostra $Atampone opo - out dello script del tutto per avviare lo script sopra dall'alto con la successiva riga di input se presente.
    • Se l' test non è riuscita faremo branch di nuovo alla :tetichetta op e recurse per un'altra linea di input - possibilmente iniziare il ciclo su se $matchsi verifica durante la raccolta $Aopo.
  • Se otteniamo oltre un $matchciclo funzione, quindi cercheremo di pRint l' $ultima riga, se questo è vero, e se !non provate a s///ubstitute per &$Bl'esimo \ncarattere ewline nello spazio modello.
    • Ci tEst questo, troppo, e se è successo ci ramo per l' :Petichetta di Rint.
    • Altrimenti torneremo all'operazione :te aggiungeremo un'altra riga di input al buffer.
  • Se ce la facciamo a :Print, ci Printaccheremo, quindi Delimineremo fino alla prima \newline nello spazio modello e eseguiremo nuovamente lo script dall'alto con ciò che rimane.

E così questa volta, se lo stessimo facendo A=2 B=2 match=5; seq 5 | sed...

Lo spazio del modello per la prima iterazione a :Print sarebbe simile a:

^1\n2\n3$

Ed è così che sedraccoglie il suo precedente $Bbuffer. E così sedstampa sull'output $Bdelle linee di conteggio dietro l'input che ha raccolto. Questo significa che, dato il nostro esempio precedente, sedsarebbero PRint 1di uscita, e poi Delete che e rispedire alla parte superiore dello script di uno spazio modello che assomiglia a:

^2\n3$

... e nella parte superiore dello script Nviene recuperata la riga di input ext e quindi la prossima iterazione appare come:

^2\n3\n4$

E così quando troviamo la prima occorrenza di 5in input, lo spazio del pattern in realtà appare come:

^3\n4\n5$

Quindi Dentra in gioco il ciclo elete e quando è finito sembra:

^5$

E quando Nviene estratta la linea di input ext, sedcolpisce EOF e si chiude. A quel punto ha sempre e solo Psfilato le righe 1 e 2.

Ecco un esempio:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Che stampa:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100

In realtà sto lavorando con file di grandi dimensioni e la risposta di don è stata notevolmente più lenta di questa soluzione. Inizialmente ero titubante nel cambiare la mia risposta accettata, ma la differenza di velocità è abbastanza evidente.
Amelio Vazquez-Reina,

4
@Amelio: funzionerà con un flusso di qualsiasi dimensione e non è necessario leggere il file per funzionare. Il più grande fattore di prestazione è la dimensione di $Ae / o $B. Più grandi diventerai quei numeri, più lentamente diventerà - ma puoi renderli ragionevolmente grandi.
Mikeserv,

1
@ AmelioVazquez-Reina - se stai usando quello più vecchio, questo è meglio, penso.
Mikeserv,

11

È possibile utilizzare gnu grepcon -Ae -Bper stampare esattamente le parti del file che si desidera escludere, ma aggiungere l' -nopzione per stampare anche i numeri di riga e quindi formattare l'output e passarlo come script di comando sedper eliminare quelle righe:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Questo dovrebbe funzionare anche con file di schemi passati greptramite -fad es .:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Penso che questo potrebbe essere leggermente ottimizzato se crollasse tre o più numeri di riga consecutivi in ​​intervalli in modo da avere ad esempio 2,6dinvece di 2d;3d;4d;5d;6d... sebbene se l'input ha solo poche corrispondenze non vale la pena farlo.


Altri modi che non mantengono l'ordine delle righe e sono probabilmente più lenti:
con comm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

commrichiede un input ordinato, il che significa che l'ordine delle righe non verrebbe conservato nell'output finale (a meno che il file non sia già ordinato), quindi nlviene utilizzato per numerare le righe prima dell'ordinamento, comm -13stampa solo le righe univoche per il 2o FILE e quindi cutrimuove la parte che è stata aggiunta da nl(ovvero il primo campo e il delimitatore :)
con join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-

Grazie Don! Domanda veloce, ti aspetteresti che la soluzione commsia più veloce di quella originale con sede grep?
Amelio Vazquez-Reina,

1
@ AmelioVazquez-Reina - Non credo perché legge ancora il file di input due volte (più fa un po 'di ordinamento) rispetto alla soluzione di Mike che elabora il file solo una volta.
don_crissti,

9

Se non ti dispiace usare vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesattiva la modalità ex silenziosa non compatibile. Utile per gli script.
  • +{command}dì a vim di essere eseguito {command}sul file.
  • g/${PAT}/- su tutte le linee corrispondenti /fff/. Questo diventa complicato se il modello contiene caratteri speciali di espressioni regolari che non intendevi trattare in quel modo.
  • .-${B} - da 1 riga sopra questa
  • .+${A}- a 2 righe sotto questa (vedi :he cmdline-rangesper queste due)
  • d - elimina le righe.
  • +w !tee quindi scrive sullo standard output.
  • +q! si chiude senza salvare le modifiche.

È possibile saltare le variabili e utilizzare direttamente il modello e i numeri. Li ho usati solo per chiarezza di scopo.


3

Che ne dici di (usando GNU grepe bash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

Qui stiamo trovando le linee da scartare grep -B2 -A1 'fff' file.txt, quindi usandole come file di input per trovare le linee desiderate scartandole.


Hmm, questo non emette nulla sulla mia macchina (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina, mi dispiace..non conoscevo il tuo sistema operativo prima ... comunque l'ho provato su Ubuntu ..
heemayl

2
Ciò avrebbe lo stesso problema della kossoluzione (ora eliminata) come se ci fossero linee duplicate nel file di input e alcune di esse non rientrano nell'intervallo e altre sono all'interno di quell'intervallo, questo le cancellerà tutte. Inoltre, con più occorrenze di pattern , se ci sono linee come --nel file di input (al di fuori degli intervalli) questo le cancellerà perché il delimitatore --appare grepnell'output quando più di una riga corrisponde al pattern (quest'ultima è altamente improbabile ma vale la pena menzionando immagino).
don_crissti,

@don_crissti Thanks..you sono right..although stavo prendendo l'esempio di OP literally..i sto andando lasciarlo nel caso qualcuno trovare utile più tardi ..
heemayl

1

Puoi ottenere un risultato abbastanza buono usando i file temporanei:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

Il risultato è abbastanza buono perché è possibile perdere un po 'di rientro nel processo, ma se si tratta di un file insensibile xml o indentazione non dovrebbe essere un problema. Poiché questo script utilizza un'unità ram, scrivere e leggere quei file temporanei è veloce come lavorare in memoria.


1

Inoltre, se si desidera escludere alcune righe prima di un determinato marker, è possibile utilizzare:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(glenn jackman su /programming//a/1492538 )

Effettuando il piping di alcuni comandi è possibile ottenere il comportamento prima / dopo:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac

1
Brillante, usa awksu un file invertito per gestire le seguenti linee quando intendi influenzare le linee prima e invertire nuovamente il risultato.
karmakaze,

0

Un modo per raggiungere questo obiettivo, forse il modo più semplice sarebbe quello di creare una variabile e fare quanto segue:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

In questo modo hai ancora la tua struttura. E puoi facilmente vedere dall'unica riga cosa stai cercando di rimuovere.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii

stessa soluzione di heemayl e stesso problema descritto da don_crissti: questo avrebbe lo stesso problema della soluzione di kos (ora cancellata) come se ci fossero linee duplicate nel file di input e alcune di esse non rientrano nell'intervallo e altre sono all'interno di quell'intervallo questo li cancellerà tutti. Inoltre, con più occorrenze di pattern, se ci sono linee come - nel file di input (al di fuori degli intervalli) questo li cancellerà perché il delimitatore - appare nell'output di grep quando più di una riga corrisponde al pattern (quest'ultimo è altamente improbabile ma vale la pena ricordare che immagino).
Bodo Thiesen,

0

Se ci sono solo 1 corrispondenza:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

Altrimenti (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
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.