Jak uzyskać część pliku po pierwszej linii, która pasuje do wyrażenia regularnego?

169

Mam plik zawierający około 1000 wierszy. Chcę, aby część mojego pliku znajdowała się po wierszu, który pasuje do mojej instrukcji grep.

To jest:

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

Tak więc chcę plik z linii 535 do linii 1000 do dalszego przetwarzania.

Jak mogę to zrobić?

Yugal Jindle
źródło
34
UUOC (Bezużyteczne wykorzystanie kota):grep 'TERMINATE' file
Jacob
30
Wiem o tym, tak jakbym tego używał w ten sposób. Wróćmy do pytania.
Yugal Jindle
3
To jest bardzo dobre pytanie programistyczne i dobrze nadaje się do przepełnienia stosu.
aioobe
13
@Jacob To wcale nie jest bezużyteczne wykorzystanie kota. Jego użycie polega na wydrukowaniu pliku na standardowe wyjście, co oznacza, że ​​możemy użyć grepstandardowego interfejsu wejściowego do wczytywania danych, zamiast konieczności uczenia się, do jakiego przełącznika zastosować grep, i sed, i awk, i pandoc, ffmpegitd., Kiedy chcemy czytać z pliku. Oszczędza to czas, ponieważ nie musimy uczyć się nowego przełącznika za każdym razem, gdy chcemy zrobić to samo: czytać z pliku.
runeks
@runeks zgadzam się z sentymentu - ale można to osiągnąć bez kota: grep 'TERMINATE' < file. Może to trochę utrudnia czytanie - ale to jest skrypt powłoki, więc to zawsze będzie problem :)
LOAS

Odpowiedzi:

307

Poniższe wypisze dopasowanie linii TERMINATEdo końca pliku:

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

Wyjaśnione: -n wyłącza domyślne zachowanie seddrukowania każdej linii po wykonaniu na niej skryptu, -ewskazany skrypt sed, /TERMINATE/,$to wybór zakresu adresów (linii), co oznacza, że ​​pierwsza linia pasuje do TERMINATEwyrażenia regularnego (jak grep) do końca pliku ( $) , i pjest poleceniem drukowania, które drukuje bieżący wiersz.

Spowoduje to wydrukowanie z linii, która następuje po dopasowaniu linii TERMINATEdo końca pliku:
(od PO dopasowaniu linii do EOF, NIE włączając pasującej linii)

sed -e '1,/TERMINATE/d'

Wyjaśnione: 1,/TERMINATE/ to wybór zakresu adresów (linii), co oznacza pierwszą linię wejścia do pierwszej linii pasującej do TERMINATEwyrażenia regularnego, i djest to polecenie usuwania, które usuwa bieżącą linię i przeskakuje do następnej. Jako seddomyślne zachowanie jest drukowanie linii, zostanie wydrukowana po linie TERMINATE na końcu wejścia.

Edytować:

Jeśli chcesz mieć wiersze przed TERMINATE:

sed -e '/TERMINATE/,$d'

A jeśli chcesz, aby obie linie przed i po TERMINATEw 2 różnych plikach w jednym przebiegu:

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

Pliki przed i po będą zawierały linię z zakończeniem, więc aby przetworzyć każdy, musisz użyć:

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

Edit2:

JEŚLI nie chcesz na stałe zakodować nazw plików w skrypcie sed, możesz:

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

Ale wtedy musisz pominąć $znaczenie ostatniej linii, aby powłoka nie spróbowała rozwinąć $wzmiennej (zwróć uwagę, że teraz używamy podwójnych cudzysłowów wokół skryptu zamiast pojedynczych cudzysłowów).

Zapomniałem powiedzieć, że nowa linia jest ważna po nazwach plików w skrypcie, aby sed wiedział, że nazwy plików się kończą.


Edycja: 2016-0530

Sébastien Clément zapytał: „Jak zamienić zakodowane na stałe TERMINATEzmienną?”

Utworzyłbyś zmienną dla pasującego tekstu, a następnie zrobiłbyś to w taki sam sposób, jak w poprzednim przykładzie:

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

aby użyć zmiennej dla pasującego tekstu z poprzednich przykładów:

## 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"

Ważne punkty dotyczące zastępowania tekstu zmiennymi w takich przypadkach to:

  1. Zmienne ( $variablename) zawarte w single quotes[ '] nie "rozszerzają się", ale zmienne wewnątrz double quotes[ "] tak. Więc trzeba zmienić wszystko single quotes, aby double quotesjeśli zawierają tekst, który chcesz zastąpić zmienną.
  2. Te sedzakresy zawierają również $i natychmiast następuje listem jak: $p, $d, $w. Będą one również wyglądać zmiennych zostać rozszerzona, więc trzeba uciekać te $znaki z backslashem [ \], takich jak: \$p, \$d, \$w.
jfg956
źródło
Jak możemy uzyskać wiersze przed TERMINATE i usunąć wszystko, co następuje?
Yugal Jindle
Jak zastąpiłbyś zakodowany na stałe TERMINAL zmienną?
Sébastien Clément
2
Jednym z przypadków użycia, którego tu brakuje, jest drukowanie wierszy po ostatnim znaczniku (jeśli w pliku może być ich wiele ... pomyśl o plikach dziennika itp.).
mato
Przykład sed -e "1,/$matchtext/d"nie działa, gdy $matchtextwystępuje w pierwszym wierszu. Musiałem to zmienić na sed -e "0,/$matchtext/d".
Karalga
61

Jako proste przybliżenie, którego możesz użyć

grep -A100000 TERMINATE file

który greps dla TERMINATEi wyprowadza do 100000 linii po tej linii.

Ze strony podręcznika

-A NUM, --after-context=NUM

Wypisuje NUM wierszy końcowego kontekstu po pasujących wierszach. Umieszcza wiersz zawierający separator grup (-) między sąsiednimi grupami dopasowań. Z opcją -o lub --only-matching nie ma to żadnego skutku i pojawia się ostrzeżenie.

aioobe
źródło
To może zadziałać, ale muszę go zakodować w swoim skrypcie, aby przetworzyć wiele plików. Pokaż więc ogólne rozwiązanie.
Yugal Jindle
3
Myślę, że to jedno praktyczne rozwiązanie!
michelgotta
2
podobnie -B LICZ, --before-context = LICZ. Wyświetla LICZBĘ wierszy wiodącego kontekstu przed dopasowanymi wierszami. Umieszcza wiersz zawierający separator grup (-) między sąsiednimi grupami dopasowań. Z opcją -o lub --only-matching nie ma to żadnego skutku i pojawia się ostrzeżenie.
PiyusG
to rozwiązanie zadziałało dla mnie, ponieważ mogę łatwo użyć zmiennych jako mojego ciągu do sprawdzenia.
Jose Martinez,
3
Dobry pomysł! Jeśli nie masz pewności co do rozmiaru kontekstu, możesz filezamiast tego policzyć wiersze :grep -A$(cat file | wc -l) TERMINATE file
Lemming
26

Narzędziem do użycia jest awk:

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

Jak to działa:

  1. Ustawiamy zmienną „znaleziona” na zero, oceniając fałsz
  2. jeśli dopasowanie „TERMINATE” zostanie znalezione w wyrażeniu regularnym, ustawiamy je na jeden.
  3. Jeśli nasza zmienna 'znaleziona' ma wartość Prawda, wypisz :)

Inne rozwiązania mogą zajmować dużo pamięci, jeśli używasz ich do bardzo dużych plików.

Jos De Graeve
źródło
Prosty, elegancki i bardzo ogólny. W moim przypadku było to drukowanie wszystkiego do drugiego wystąpienia '###':cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek 16.08.17
3
Narzędziem, którego tutaj nie należy używać, jest cat. awkjest doskonale w stanie przyjąć jedną lub więcej nazw plików jako argumenty. Zobacz także stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee Sierpnia
9

Jeśli dobrze rozumiem pytanie prawidłowej chcą linie po TERMINATE , nie licząc TERMINATE-line. awkmożna to zrobić w prosty sposób:

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

Wyjaśnienie:

  1. Chociaż nie jest to najlepsza praktyka, możesz polegać na fakcie, że wszystkie zmienne mają domyślnie wartość 0 lub pusty ciąg, jeśli nie został zdefiniowany. Zatem pierwsze wyrażenie ( if(found) print) nie wypisze niczego, od czego można by zacząć.
  2. Po zakończeniu drukowania sprawdzamy, czy jest to linia startowa (której nie należy uwzględniać).

Będzie to wydrukować wszystkie linie po tym TERMINATE-line.


Uogólnienie:

  • Masz plik z liniami początkowymi i końcowymi i chcesz, aby linie między tymi wierszami były wykluczone z linii początkowej i końcowej .
  • linie początkowe i końcowe można zdefiniować za pomocą wyrażenia regularnego pasującego do wiersza.

Przykład:

$ 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
$

Wyjaśnienie:

  1. Jeśli zostanie znaleziona końcowa linia, nie należy drukować. Zauważ, że to sprawdzenie jest wykonywane przed faktycznym drukowaniem, aby wykluczyć końcową linię z wyniku.
  2. Drukuj bieżący wiersz, jeśli foundjest ustawiony.
  3. Jeśli zostanie znaleziona linia początkowa, ustaw ją found=1tak, aby drukowane były następujące wiersze. Zauważ, że to sprawdzenie jest wykonywane po faktycznym wydrukowaniu, aby wykluczyć wiersz początkowy z wyniku.

Uwagi:

  • Kod opiera się na fakcie, że wszystkie awk-vars przyjmują domyślnie 0 lub pusty łańcuch, jeśli nie są zdefiniowane. Jest to poprawne, ale może nie być najlepszą praktyką, więc możesz dodać a BEGIN{found=0}na początku wyrażenia awk.
  • Jeśli zostanie znalezionych wiele bloków początku-końca , wszystkie zostaną wydrukowane.
UlfR
źródło
1
Niesamowity niesamowity przykład. Właśnie spędziłem 2 godziny na oglądaniu csplit, sed i wszelkiego rodzaju skomplikowanych poleceń awk. Nie tylko zrobiło to, co chciałem, ale pokazało się na tyle prosto, aby wywnioskować, jak zmodyfikować to, aby zrobić kilka innych powiązanych rzeczy, których potrzebowałem. Przypomina mi, że awk jest świetny, a nie tylko w nieczytelnym bałaganie. Dzięki.
user1169420
{if(found) print}jest trochę anty-wzorzec w awk, bardziej idiomatyczne jest zastąpienie bloku tylko foundlub found;później, jeśli będzie potrzebny inny filtr.
user000001
@ user000001 proszę wyjaśnić. Nie rozumiem co i jak wymienić. W każdym razie myślę, że sposób, w jaki jest napisany, bardzo jasno pokazuje, co się dzieje.
UlfR
1
Można by wymienić awk '{if(found) print} /TERMINATE/{found=1}' your_filez awk 'found; /TERMINATE/{found=1}' your_file, należy oba robią to samo.
user000001
7

Użyj rozwinięcia parametrów bash w następujący sposób:

content=$(cat file)
echo "${content#*TERMINATE}"
Mu Qiao
źródło
Czy możesz wyjaśnić, co robisz?
Yugal Jindle
Skopiowałem zawartość "file" do zmiennej $ content. Następnie usunąłem wszystkie znaki, aż do wyświetlenia „TERMINATE”. Nie używał zachłannego dopasowywania, ale możesz użyć chciwego dopasowywania przez $ {content ## * TERMINATE}.
Mu Qiao,
tutaj jest link do podręcznika basha: gnu.org/software/bash/manual/…
Mu Qiao
6
co się stanie, jeśli plik ma rozmiar 100 GB?
Znik
1
Głos przeciw: To jest okropne (wczytywanie pliku do zmiennej) i złe (używanie zmiennej bez jej cytowania; i powinieneś właściwie używać printflub upewnić się, że wiesz dokładnie, do czego przekazujesz echo.).
tripleee
6

grep -A 10000000 Plik 'TERMINATE'

  • jest dużo, dużo szybszy niż sed, zwłaszcza przy pracy z naprawdę dużym plikiem. Działa do 10 milionów linii (lub cokolwiek włożysz), więc nie ma nic złego w uczynieniu tego wystarczająco dużym, aby poradzić sobie z każdym trafieniem.
user8910163
źródło
4

Istnieje wiele sposobów, aby to zrobić z sedalbo awk:

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

Szuka TERMINATEw Twoim pliku i drukuje od tego wiersza do końca pliku.

awk '/TERMINATE/,0' file

To jest dokładnie to samo zachowanie co sed.

W przypadku, gdy znasz numer linii, od której chcesz rozpocząć drukowanie, możesz podać go razem z NR(numer rekordu, który ostatecznie określa numer linii):

awk 'NR>=535' file

Przykład

$ 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
fedorqui 'SO przestań szkodzić'
źródło
Jako numer, którego możesz również użyćmore +7 file
123
Obejmuje to pasującą linię, która nie jest tym, o co chodzi w tym pytaniu.
mivk
@mivk cóż, dotyczy to również zaakceptowanej odpowiedzi i drugiej najczęściej ocenianej odpowiedzi, więc problem może dotyczyć wprowadzającego w błąd tytułu.
fedorqui „SO przestań szkodzić”
3

Jeśli z jakiegoś powodu chcesz uniknąć używania seda, poniższe wypisze pasującą linię TERMINATEdo końca pliku:

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

a następujący wiersz zostanie wydrukowany od następującego dopasowania wiersza TERMINATEdo końca pliku:

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

Potrzeba dwóch procesów, aby zrobić to, co sed może zrobić w jednym procesie, a jeśli plik zmieni się między wykonaniem grep i tail, wynik może być niespójny, więc zalecam użycie seda. Ponadto, jeśli plik nie zawiera TERMINATE, pierwsze polecenie kończy się niepowodzeniem.

jfg956
źródło
plik jest skanowany dwukrotnie. co jeśli ma rozmiar 100 GB?
Znik
1
Głosowano negatywnie, ponieważ jest to kiepskie rozwiązanie, ale potem głosowano za, ponieważ 90% odpowiedzi to zastrzeżenia.
Mad Physicist,
0

To może być jeden ze sposobów na zrobienie tego. Jeśli wiesz, w której linii pliku masz słowo grep i ile wierszy masz w swoim pliku:

grep -A466 Plik 'TERMINATE'

Mariah
źródło
1
Jeśli numer linii jest znany, grepnie jest nawet wymagany; możesz po prostu użyć tail -n $NUM, więc to naprawdę nie jest odpowiedź.
Samveen
-1

sed jest znacznie lepszym narzędziem do tego zadania: plik sed -n '/ re /, $ p'

gdzie re to regexp.

Inną opcją jest flaga grepa --after-context. Musisz przekazać liczbę na końcu, użycie wc na pliku powinno dać właściwą wartość zatrzymania. Połącz to z -n i swoim wyrażeniem dopasowującym.

ckwang
źródło
--after-context jest w porządku, ale nie we wszystkich przypadkach.
Yugal Jindle
Czy możesz zasugerować coś innego .. ??
Yugal Jindle
-2

Spowoduje to wyświetlenie wszystkich wierszy od ostatniej znalezionej linii „TERMINATE” do końca pliku:

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
easyyu
źródło
Wyodrębnianie numeru wiersza za pomocą, grepaby można go było tailpodać, jest marnotrawnym antywzorem. Znalezienie dopasowania i wydrukowanie końca pliku (lub odwrotnie, wydrukowanie i zatrzymanie się na pierwszym dopasowaniu) jest wybitnie wykonywane za pomocą zwykłych, podstawowych narzędzi regex. Masywność grep | tail | sed | awkjest również sama w sobie masowym bezużytecznym wykorzystaniem grepprzyjaciół i przyjaciół .
tripleee
Myślę, że s * próbował nam podać coś, co mogłoby znaleźć / ostatnią instancję / elementu „TERMINATE” i podać wiersze z tego wystąpienia. Inne implementacje dają pierwszeństwo do przodu. LINE_NUMBER powinien prawdopodobnie wyglądać tak, zamiast tego: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Może nie jest to najbardziej elegancki sposób, ale tak wydaje się, że wykonuje swoją pracę. ^. ^
fbicknel
... lub wszystko w jednej linii, ale brzydkie: tail -n + $ (grep -o -n 'TERMINATE' $ NAZWA_PLIKU | ogon -n 1 | awk -F: '{print $ 1}') $ NAZWA_PLIKU
fbicknel
.... i miałem zamiar wrócić i edytować $ OSCAM_LOG zamiast $ YOUR_FILE_NAME ... ale z jakiegoś powodu nie mogę. Nie mam pojęcia, skąd pochodzi $ OSCAM_LOG; Po prostu bezmyślnie to papugowałem. oO
fbicknel
Robienie tego w samym Awk jest częstym zadaniem w Awk 101. Jeśli już używasz bardziej wydajnego narzędzia tylko do uzyskania numeru linii, puść taili wykonaj zadanie w bardziej wydajnym narzędziu. Zresztą tytuł wyraźnie mówi „pierwszy mecz”.
tripleee