Czy grep może wyświetlać tylko określone grupy, które pasują?

289

Powiedz, że mam plik:

# file: 'test.txt'
foobar bash 1
bash
foobar happy
foobar

Chcę tylko wiedzieć, jakie słowa pojawiają się po „foobar”, więc mogę użyć tego wyrażenia regularnego:

"foobar \(\w\+\)"

Nawiasy wskazują, że szczególnie interesuję się tym słowem zaraz po foobar. Ale kiedy robię a grep "foobar \(\w\+\)" test.txt, otrzymuję całe wiersze, które pasują do całego wyrażenia regularnego, a nie tylko „słowo po foobar”:

foobar bash 1
foobar happy

Wolałbym, aby wynik tego polecenia wyglądał następująco:

bash
happy

Czy istnieje sposób, aby powiedzieć grepowi, aby wyświetlał tylko elementy pasujące do grupy (lub określonej grupy) w wyrażeniu regularnym?

Cory Klein
źródło
4
dla tych, którzy nie potrzebują grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
sklepienie

Odpowiedzi:

324

GNU grep ma -Popcję dla wyrażeń regularnych w stylu perla i -oopcję drukowania tylko tego, co pasuje do wzorca. Można je łączyć za pomocą asertywnych stwierdzeń (opisanych w części Rozszerzone wzorce na stronie podręcznika perlre ), aby usunąć część wzoru grep z tego, co do którego stwierdzono, że jest dopasowane -o.

$ grep -oP 'foobar \K\w+' test.txt
bash
happy
$

Jest \Kto krótka (i bardziej wydajna) forma, (?<=pattern)której używasz jako asertywnego potwierdzenia zerowej szerokości przed tekstem, który chcesz wydrukować. (?=pattern)może być używany jako asertywne stwierdzenie o zerowej szerokości po tekście, który chcesz wydrukować.

Na przykład, jeśli chcesz dopasować słowo pomiędzy fooi bar, możesz użyć:

$ grep -oP 'foo \K\w+(?= bar)' test.txt

lub (dla symetrii)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
źródło
3
Jak to zrobić, jeśli wyrażenie regularne ma więcej niż grupę? (jak sugeruje tytuł?)
barracel
4
@barracel: Nie wierzę, że możesz. Czas nased(1)
camh
1
@camh Właśnie przetestowałem, że grep -oP 'foobar \K\w+' test.txtnic nie wychodzi z PO test.txt. Wersja grep to 2.5.1. Co może być nie tak? O_O
SOUser
@XichenLi: Nie mogę powiedzieć. Właśnie zbudowałem wersję 2.5.1 grep (jest dość stary - od 2006 roku) i działało dla mnie.
camh
@SOUser: Doświadczyłem tego samego - nic nie zapisuje do pliku. Wysłałem żądanie edycji, aby dołączyć „>” przed nazwą pliku, aby wysłać dane wyjściowe, ponieważ to działało dla mnie.
rjchicago
39

Standardowy grep nie może tego zrobić, ale najnowsze wersje GNU grep mogą . Możesz zmienić tryb na sed, awk lub perl. Oto kilka przykładów, które robią, co chcesz na przykładowych danych wejściowych; zachowują się nieco inaczej w przypadkach narożnych.

Zamień foobar word other stuffna word, drukuj tylko po dokonaniu wymiany.

sed -n -e 's/^foobar \([[:alnum:]]\+\).*/\1/p'

Jeśli pierwszym słowem jest foobar, wydrukuj drugie słowo.

awk '$1 == "foobar" {print $2}'

Usuń, foobarjeśli jest to pierwsze słowo, w przeciwnym razie pomiń wiersz; następnie usuń wszystko po pierwszej spacji i wydrukuj.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
źródło
Niesamowite! Myślałem, że mogę to zrobić za pomocą sed, ale nie użyłem tego wcześniej i miałem nadzieję, że będę mógł użyć mojego znajomego grep. Ale składnia tych poleceń wygląda teraz bardzo znajomo, ponieważ jestem zaznajomiony z wyszukiwaniem i zastępowaniem + wyrażeń regularnych w stylu vim. Wielkie dzięki.
Cory Klein
1
To nieprawda, Gilles. Zobacz moją odpowiedź na rozwiązanie GNU grep.
camh
1
@camh: Ach, nie wiedziałem, że GNU grep ma teraz pełną obsługę PCRE. Poprawiłem swoją odpowiedź, dzięki.
Gilles,
1
Ta odpowiedź jest szczególnie przydatna w przypadku wbudowanego systemu Linux, ponieważ Busybox grepnie obsługuje PCRE.
Craig McQueen
Oczywiście istnieje wiele sposobów na wykonanie tego samego zadania, jednak jeśli OP poprosi o użycie grep, dlaczego odpowiesz na coś innego? Również pierwszy akapit jest niepoprawny: tak grep może to zrobić.
fcm
32
    sed -n "s/^.*foobar\s*\(\S*\).*$/\1/p"

-n     suppress printing
s      substitute
^.*    anything before foobar
foobar initial search match
\s*    any white space character (space)
\(     start capture group
\S*    capture any non-white space character (word)
\)     end capture group
.*$    anything after the capture group
\1     substitute everything with the 1st capture group
p      print it
jgshawkey
źródło
1
+1 dla przykładu sed wydaje się lepszym narzędziem do pracy niż grep. Jeden komentarz, ^i $są obce, ponieważ .*jest chciwy mecz. Jednak włączenie ich może pomóc wyjaśnić cel wyrażenia regularnego.
Tony
18

Cóż, jeśli wiesz, że foobar jest zawsze pierwszym słowem lub linią, możesz użyć cut. Tak jak:

grep "foobar" test.file | cut -d" " -f2
Dave
źródło
-oWłącz grep jest powszechnie wdrażane (moreso niż rozszerzeniach GNU grep), więc robi grep -o "foobar" test.file | cut -d" " -f2zwiększy skuteczność tego rozwiązania, które jest bardziej mobilny niż przy użyciu lookbehind twierdzeń.
dubiousjim
Wierzę, że będziesz potrzebować grep -o "foobar .*"lub grep -o "foobar \w+".
G-Man
9

Jeśli PCRE nie jest obsługiwane, możesz osiągnąć ten sam wynik za pomocą dwóch wywołań grep. Na przykład, aby złapać słowo po foobar, wykonaj następujące czynności:

<test.txt grep -o 'foobar  *[^ ]*' | grep -o '[^ ]*$'

Można go rozwinąć do dowolnego słowa po foobar, takiego jak ten (z ERE dla czytelności):

i=1
<test.txt egrep -o 'foobar +([^ ]+ +){'$i'}[^ ]+' | grep -o '[^ ]*$'

Wynik:

1

Zauważ, że indeks ijest zerowy.

Thor
źródło
6

pcregrepma inteligentniejszą -oopcję, która pozwala wybrać grupy przechwytywania, które chcesz wydrukować. Korzystając z pliku przykładowego,

$ pcregrep -o1 "foobar (\w+)" test.txt
bash
happy
G-Man
źródło
4

Korzystanie grepnie jest kompatybilne z wieloma platformami, ponieważ -P/ --perl-regexpjest dostępne tylko na GNUgrep , a nie na BSDgrep .

Oto rozwiązanie wykorzystujące ripgrep:

$ rg -o "foobar (\w+)" -r '$1' <test.txt
bash
happy

Zgodnie z man rg:

-r/ --replace REPLACEMENT_TEXTZastąp każdy mecz podanym tekstem.

Indeksy grup przechwytywania (np. $5) I nazwy (np. $foo) Są obsługiwane w ciągu zastępującym.

Powiązane: GH-462 .

kenorb
źródło
2

Odpowiedź @jgshawkey była dla mnie bardzo pomocna. grepnie jest tak dobrym narzędziem do tego, ale sed jest, chociaż tutaj mamy przykład, który używa grep, aby uchwycić odpowiednią linię.

Składnia regex sed jest idiosynkratyczna, jeśli nie jesteś do tego przyzwyczajony.

Oto inny przykład: ten analizuje dane wyjściowe xinput, aby uzyskać liczbę całkowitą ID

⎜   ↳ SynPS/2 Synaptics TouchPad                id=19   [slave  pointer  (2)]

i chcę 19

export TouchPadID=$(xinput | grep 'TouchPad' | sed  -n "s/^.*id=\([[:digit:]]\+\).*$/\1/p")

Zwróć uwagę na składnię klas:

[[:digit:]]

i potrzeba ucieczki przed następującymi +

Zakładam, że pasuje tylko jedna linia.

Tim Richardson
źródło
Właśnie to starałem się zrobić. Dzięki!
James
Nieco łatwiejsza wersja bez dodatkowych grep, zakładając, że „TouchPad” znajduje się po lewej stronie „id”:echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu