Come ottenere la parte di un file dopo la prima riga che corrisponde a un'espressione regolare?


169

Ho un file con circa 1000 righe. Voglio la parte del mio file dopo la riga che corrisponde alla mia dichiarazione grep.

Questo è:

$ cat file | grep 'TERMINATE'     # It is found on line 534

Quindi, voglio il file dalla riga 535 alla riga 1000 per ulteriori elaborazioni.

Come lo posso fare?


34
UUOC (Uso inutile del gatto):grep 'TERMINATE' file
Jacob,

30
Lo so, è come se lo usassi in quel modo. Torniamo alla domanda.
Yugal Jindle,

3
Questa è una domanda di programmazione perfettamente adatta e adatta allo stackoverflow.
aioobe,

13
@Jacob Non è affatto inutile usare il gatto. Il suo uso è stampare un file sull'output standard, il che significa che possiamo usare grepl'interfaccia di input standard per leggere i dati, piuttosto che dover imparare a quale interruttore applicare grep, e sed, e awk, e pandoc, ffmpegecc. Quando vogliamo leggere da un file. Risparmia tempo perché non dobbiamo imparare un nuovo interruttore ogni volta che vogliamo fare la stessa cosa: leggere da un file.
Runeks,

@runeks Sono d'accordo con il tuo sentimento - ma è possibile raggiungere questo senza catalizzatore: grep 'TERMINATE' < file. Forse rende la lettura un po 'più difficile - ma questo è lo scripting della shell, quindi sarà sempre un problema :)
LOAS

Risposte:


307

Di seguito verrà stampata la corrispondenza della riga TERMINATEfino alla fine del file:

sed -n -e '/TERMINATE/,$p'

Spiegazione: -n disabilita il comportamento predefinito seddi stampa di ogni riga dopo aver eseguito il suo script su di essa, -eindicato uno script su sed, /TERMINATE/,$è una selezione di intervallo di indirizzi (riga) che significa che la prima riga che corrisponde TERMINATEall'espressione regolare (come grep) alla fine del file ( $) ed pè il comando di stampa che stampa la riga corrente.

Questo verrà stampato dalla riga che segue la corrispondenza della riga TERMINATEfino alla fine del file:
(da DOPO la riga corrispondente a EOF, NON compresa la riga corrispondente)

sed -e '1,/TERMINATE/d'

Spiegazione: 1,/TERMINATE/ è una selezione dell'intervallo di indirizzi (riga) che indica la prima riga per l'input alla prima riga corrispondente TERMINATEall'espressione regolare, ed dè il comando di eliminazione che elimina la riga corrente e passa alla riga successiva. Poiché sedil comportamento predefinito è di stampare le righe, le righe verranno stampate dopo TERMINATE fino alla fine dell'input.

Modificare:

Se vuoi le linee prima TERMINATE:

sed -e '/TERMINATE/,$d'

E se vuoi entrambe le righe prima e dopo TERMINATEin 2 file diversi in un unico passaggio:

sed -e '1,/TERMINATE/w before
/TERMINATE/,$w after' file

I file before e after conterranno la riga con terminate, quindi per elaborare ciascuno devi usare:

head -n -1 before
tail -n +2 after

Edit2:

Se non si desidera codificare i nomi dei file nello script sed, è possibile:

before=before.txt
after=after.txt
sed -e "1,/TERMINATE/w $before
/TERMINATE/,\$w $after" file

Ma poi devi sfuggire al $significato dell'ultima riga in modo che la shell non proverà ad espandere la $wvariabile (nota che ora usiamo le virgolette doppie attorno allo script invece delle virgolette singole).

Ho dimenticato di dire che la nuova riga è importante dopo i nomi dei file nella sceneggiatura, in modo che sed sappia che i nomi dei file finiscono.


Modifica: 30/05/2016

Sébastien Clément ha chiesto: "Come sostituiresti l'hardcoded TERMINATEcon una variabile?"

Dovresti creare una variabile per il testo corrispondente e farlo allo stesso modo dell'esempio precedente:

matchtext=TERMINATE
before=before.txt
after=after.txt
sed -e "1,/$matchtext/w $before
/$matchtext/,\$w $after" file

utilizzare una variabile per il testo corrispondente con gli esempi precedenti:

## Print the line containing the matching text, till the end of the file:
## (from the matching line to EOF, including the matching line)
matchtext=TERMINATE
sed -n -e "/$matchtext/,\$p"
## Print from the line that follows the line containing the 
## matching text, till the end of the file:
## (from AFTER the matching line to EOF, NOT including the matching line)
matchtext=TERMINATE
sed -e "1,/$matchtext/d"
## Print all the lines before the line containing the matching text:
## (from line-1 to BEFORE the matching line, NOT including the matching line)
matchtext=TERMINATE
sed -e "/$matchtext/,\$d"

I punti importanti sulla sostituzione del testo con variabili in questi casi sono:

  1. Le variabili ( $variablename) racchiuse tra single quotes[ '] non "si espandono", ma le variabili all'interno di double quotes[ "] lo faranno. Quindi, si deve cambiare tutto il single quotesper double quotesse contengono testo che si desidera sostituire con una variabile.
  2. Gli sedintervalli contengono anche una $e sono immediatamente seguiti da una lettera come: $p, $d, $w. Essi potranno anche guardare come le variabili di essere ampliato, in modo da avere per sfuggire quei $personaggi con un backslash [ \] come: \$p, \$d, \$w.

Come possiamo ottenere le righe prima di TERMINATE ed eliminare tutto ciò che segue?
Yugal Jindle,

Come sostituiresti il ​​TERMINAL hardcoded con una variabile?
Sébastien Clément,

2
Un caso d'uso che manca qui è come stampare le righe dopo l'ultimo marcatore (se nel file possono essercene più di una volta ... pensa ai file di registro ecc.).
mato,

L'esempio sed -e "1,/$matchtext/d"non funziona quando si $matchtextverifica nella prima riga. Ho dovuto cambiarlo in sed -e "0,/$matchtext/d".
Karalga,

61

Come semplice approssimazione potresti usare

grep -A100000 TERMINATE file

che esegue la grepping TERMINATEe genera fino a 100000 linee seguendo quella linea.

Dalla pagina man

-A NUM, --after-context=NUM

Stampa NUM righe del contesto finale dopo aver abbinato le righe. Posiziona una linea contenente un separatore di gruppo (-) tra gruppi contigui di corrispondenze. Con l'opzione -o o --only-matching, questo non ha alcun effetto e viene dato un avviso.


Potrebbe funzionare per questo, ma ho bisogno di codificarlo nel mio script per elaborare molti file. Quindi, mostra qualche soluzione generica.
Yugal Jindle il

3
Penso che questa sia una soluzione pratica!
michelgotta,

2
allo stesso modo -B NUM, --before-context = NUM ​​Stampa NUM righe del contesto iniziale prima di abbinare le righe. Posiziona una linea contenente un separatore di gruppo (-) tra gruppi contigui di corrispondenze. Con l'opzione -o o --only-matching, questo non ha alcun effetto e viene dato un avviso.
PiyusG

questa soluzione ha funzionato per me perché posso facilmente usare le variabili come stringa per verificare.
Jose Martinez,

3
Bella idea! Se non sei sicuro delle dimensioni del contesto, puoi invece contare le righe di file:grep -A$(cat file | wc -l) TERMINATE file
Lemming

26

Uno strumento da usare qui è awk:

cat file | awk 'BEGIN{ found=0} /TERMINATE/{found=1}  {if (found) print }'

Come funziona:

  1. Impostiamo la variabile 'found' su zero, valutando false
  2. se viene trovata una corrispondenza per "TERMINATE" con l'espressione regolare, la impostiamo su una.
  3. Se la nostra variabile "trovato" restituisce True, stampa :)

Le altre soluzioni potrebbero consumare molta memoria se le si utilizza su file molto grandi.


Semplice, elegante e molto generico. Nel mio caso stava stampando tutto fino alla seconda occorrenza di '###':cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek,

3
Uno strumento da non usare qui è cat. awkè perfettamente in grado di prendere uno o più nomi di file come argomenti. Vedere anche stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

9

Se capisco correttamente la tua domanda, vuoi le righe dopo TERMINATE , senza includere la linea TERMINATE. awkpuoi farlo in modo semplice:

awk '{if(found) print} /TERMINATE/{found=1}' your_file

Spiegazione:

  1. Sebbene non sia una buona pratica, puoi fare affidamento sul fatto che tutte le varianti predefinite sono 0 o la stringa vuota se non definita. Quindi la prima espressione ( if(found) print) non stamperà nulla con cui iniziare.
  2. Al termine della stampa, controlliamo se questa è la linea di partenza (che non dovrebbe essere inclusa).

Questo stamperà tutte le righe dopo la linea TERMINATE.


Generalizzazione:

  • Si dispone di un file con inizio e fine e si desidera che le linee tra quelle righe escludano l' inizio e le fine .
  • inizio - e le linee di fine potrebbero essere definite da un'espressione regolare corrispondente alla linea.

Esempio:

$ cat ex_file.txt 
not this line
second line
START
A good line to include
And this line
Yep
END
Nope more
...
never ever
$ awk '/END/{found=0} {if(found) print} /START/{found=1}' ex_file.txt 
A good line to include
And this line
Yep
$

Spiegazione:

  1. Se il fine è trovato -line dovrebbe essere fatto senza la stampa. Si noti che questo controllo viene eseguito prima della stampa effettiva per escludere la fine del risultato.
  2. Stampa la riga corrente se foundè impostata.
  3. Se il Iniziamo -line si trova quindi impostare found=1in modo che le righe seguenti vengono stampati. Si noti che questo controllo viene eseguito dopo la stampa effettiva per escludere la linea di partenza dal risultato.

Appunti:

  • Il codice si basa sul fatto che tutte le impostazioni predefinite di awk-vars sono 0 o la stringa vuota se non definita. Questo è valido ma potrebbe non essere la migliore pratica, quindi potresti aggiungere BEGIN{found=0}a all'inizio dell'espressione awk.
  • Se vengono rilevati più blocchi di inizio-fine , vengono stampati tutti.

1
Fantastico Fantastico esempio. Ho appena trascorso 2 ore a guardare csplit, sed e tutti i tipi di complicati comandi awk. Non solo ha fatto quello che volevo, ma ha dimostrato abbastanza semplice da dedurre come modificarlo per fare alcune altre cose correlate di cui avevo bisogno. Mi fa ricordare che Awk è fantastico e non solo in un caos indecifrabile. Grazie.
user1169420,

{if(found) print}è un po 'un anti-pattern in awk, è più idiomatico sostituire il blocco con solo foundo found;se hai bisogno di un altro filtro in seguito.
user000001

@ user000001 per favore spiegare. Non capisco cosa sostituire e come. Ad ogni modo, penso che il modo in cui è scritto chiarisca cosa sta succedendo.
UlfR,

1
Sostituiresti awk '{if(found) print} /TERMINATE/{found=1}' your_filecon awk 'found; /TERMINATE/{found=1}' your_file, entrambi dovrebbero fare la stessa cosa.
user000001

7

Utilizzare l'espansione del parametro bash come il seguente:

content=$(cat file)
echo "${content#*TERMINATE}"

Puoi spiegare cosa stai facendo?
Yugal Jindle il

Ho copiato il contenuto di "file" nella variabile $ content. Quindi ho rimosso tutti i personaggi fino a quando non è stato visualizzato "TERMINATE". Non ha usato una corrispondenza avida, ma puoi usare una corrispondenza avida di $ {content ## * TERMINATE}.
Mu Qiao,

ecco il link del manuale di bash: gnu.org/software/bash/manual/…
Mu Qiao

6
cosa succederà se il file ha una dimensione di 100 GB?
Znik,

1
Downvote: questo è orribile (leggere il file in una variabile) e sbagliato (usare la variabile senza citarla; e dovresti usare correttamente printfo assicurarti di sapere esattamente cosa stai passando echo.).
tripla il

6

grep -A 10000000 'TERMINATE' file

  • è molto, molto più veloce di sed, specialmente lavorando su file molto grandi. Funziona fino a 10 M di linee (o qualunque cosa tu abbia inserito) quindi nessun danno nel renderlo abbastanza grande da gestire qualsiasi cosa tu colpisca.

4

Esistono molti modi per farlo con sedo awk:

sed -n '/TERMINATE/,$p' file

Questo cerca TERMINATEnel tuo file e stampa da quella riga fino alla fine del file.

awk '/TERMINATE/,0' file

Questo è esattamente lo stesso comportamento di sed.

Nel caso in cui si conosca il numero della linea da cui si desidera iniziare la stampa, è possibile specificarlo insieme a NR(numero di record, che alla fine indica il numero della linea):

awk 'NR>=535' file

Esempio

$ seq 10 > a        #generate a file with one number per line, from 1 to 10
$ sed -n '/7/,$p' a
7
8
9
10
$ awk '/7/,0' a
7
8
9
10
$ awk 'NR>=7' a
7
8
9
10

Per il numero puoi anche usaremore +7 file
123

Ciò include la riga corrispondente, che non è ciò che si desidera in questa domanda.
marzo

@mivk bene, questo è anche il caso della risposta accettata e della seconda più votata, quindi il problema potrebbe essere con un titolo fuorviante.
fedorqui "SO smettere di danneggiare" il

3

Se per qualsiasi motivo, si desidera evitare di utilizzare sed, quanto segue stamperà la corrispondenza della riga TERMINATEfino alla fine del file:

tail -n "+$(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)" file

e quanto segue verrà stampato dalla seguente riga corrispondente TERMINATEfino alla fine del file:

tail -n "+$(($(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)+1))" file

Ci vogliono 2 processi per fare ciò che sed può fare in un processo, e se il file cambia tra l'esecuzione di grep e tail, il risultato può essere incoerente, quindi consiglio di usare sed. Inoltre, se il file non contiene TERMINATE, il primo comando ha esito negativo.


il file viene scansionato due volte. cosa succede se ha una dimensione di 100 GB?
Znik,

1
Sottovalutato perché questa è una soluzione scadente, ma poi votato perché il 90% della risposta è avvertenze.
Fisico pazzo,


0

Questo potrebbe essere un modo per farlo. Se sai quale riga del file hai la tua parola grep e quante righe hai nel tuo file:

grep -A466 File 'TERMINATE'


1
Se il numero di riga è noto, grepnon è nemmeno richiesto; puoi semplicemente usare tail -n $NUM, quindi questa non è davvero una risposta.
Samveen,

-1

sed è uno strumento molto migliore per il lavoro: file sed -n '/ re /, $ p'

dove re è regexp.

Un'altra opzione è il flag --after-context di grep. Devi passare un numero per terminare a, usando wc sul file dovrebbe dare il giusto valore per fermarti a. Combina questo con -n e la tua espressione di corrispondenza.


--after-context va bene, ma non in tutti i casi.
Yugal Jindle il

Puoi suggerire qualcos'altro .. ??
Yugal Jindle,

-2

Verranno stampate tutte le righe dall'ultima riga trovata "TERMINATE" fino alla fine del file:

LINE_NUMBER=`grep -o -n TERMINATE $OSCAM_LOG|tail -n 1|sed "s/:/ \\'/g"|awk -F" " '{print $1}'`
tail -n +$LINE_NUMBER $YOUR_FILE_NAME

Estrarre un numero di riga con in grepmodo da poterlo alimentare tailè un antipattern dispendioso. Trovare la corrispondenza e stampare fino alla fine del file (o, al contrario, stampare e fermarsi alla prima corrispondenza) viene eminentemente fatto con gli stessi normali strumenti regex essenziali. Il massiccio grep | tail | sed | awkè anche di per sé un massiccio uso inutile di grepe amici .
tripleee,

Penso che * stesse provando a darci qualcosa che avrebbe trovato / l'ultima istanza / di 'TERMINATE' e avrebbe dato le righe da quell'istanza in poi. Altre implementazioni forniscono la prima istanza in poi. Il LINE_NUMBER dovrebbe probabilmente apparire così, invece: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Forse non è il modo più elegante, ma è sembra aver fatto il lavoro. ^. ^
fbicknel,

... o tutto in una riga, ma brutto: tail -n + $ (grep -o -n 'TERMINATE' $ YOUR_FILE_NAME | tail -n 1 | awk -F: '{print $ 1}') $ YOUR_FILE_NAME
fbicknel

.... e stavo per tornare indietro e modificare $ OSCAM_LOG al posto di $ YOUR_FILE_NAME ... ma non posso farlo per qualche motivo. Nessuna idea da dove proviene $ OSCAM_LOG; L'ho pappagallo senza pensarci. oO
fbicknel,

Fare questo in Awk da solo è un'attività comune in Awk 101. Se si sta già utilizzando uno strumento più capace solo per ottenere il numero di riga, lasciarsi andare taile svolgere l'attività nello strumento più capace del tutto. Ad ogni modo, il titolo dice chiaramente "prima partita".
tripla
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.