Чи може виводити греп лише ті визначені угрупування, які відповідають?


290

Скажіть, у мене є файл:

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

Мені хочеться лише знати, які слова з’являються після "foobar", тому я можу використовувати цей регулярний вираз:

"foobar \(\w\+\)"

Дужки вказують на те, що я маю особливий інтерес до цього слова відразу після foobar. Але коли я роблю a grep "foobar \(\w\+\)" test.txt, я отримую цілі рядки, які відповідають цілому регулярному вираженню, а не просто "слово після foobar":

foobar bash 1
foobar happy

Я б хотів, щоб результат цієї команди виглядав так:

bash
happy

Чи є спосіб сказати grep тільки виводити елементи, що відповідають групуванню (або певній групі) у звичайному виразі?


4
для тих, хто не потребує грепу:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
Сейф

Відповіді:


325

GNU grep має -Pможливість для регекерів стилю perl, а також -oможливість друкувати лише те, що відповідає шаблону. Їх можна поєднувати, використовуючи твердження, що розглядаються (описані в Розширених шаблонах на сторінці perlre ), щоб видалити частину шаблону грепа з того, що визначено для відповідності цілям -o.

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

\KЄ короткою формою (і більш ефективна форма) , (?<=pattern)яка використовується в якості нульової ширини насторожі за твердження перед текстом , який потрібно для виведення. (?=pattern)може використовуватися як твердження вперед-нульової ширини після тексту, який ви хочете вивести.

Наприклад, якщо ви хочете відповідати слову між fooі bar, ви можете використовувати:

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

або (для симетрії)

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

3
Як це зробити, якщо ваш регекс має більше, ніж групування? (як випливає з назви?)
barracel

4
@barracel: Я не вірю, що ти можеш. Часsed(1)
до

1
@camh Я щойно перевірив, що grep -oP 'foobar \K\w+' test.txtнічого не дає з ОП test.txt. Версія grep - 2.5.1. Що може бути не так? О_О
SOUser

@XichenLi: Я не можу сказати. Я щойно побудував v2.5.1 grep (він досить старий - з 2006 року), і він працював на мене.
camh

@SOUser: Я пережив те саме - не видає нічого для файлу. Я надіслав запит на редагування, щоб включити '>' перед ім'ям файлу, щоб надіслати вихід, оскільки це працювало для мене.
rjchicago

39

Стандартний греп не може цього зробити, але останні версії GNU grep можуть . Ви можете звернутися до sed, awk або perl. Ось кілька прикладів, які роблять все, що ви хочете, на своєму прикладі зразка; у кутових випадках вони поводяться дещо інакше.

Замініть foobar word other stuffна word, друкуйте лише в тому випадку, якщо виконана заміна.

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

Якщо перше слово є foobar, надрукуйте друге слово.

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

Стріпте, foobarякщо це перше слово, і пропустіть рядок інакше; потім зніміть усе після першого пробілу та роздрукуйте.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'

Дивовижно! Я думав, що я можу це зробити з sed, але я раніше не користувався цим і сподівався, що зможу використати знайоме grep. Але синтаксис цих команд насправді виглядає дуже знайомим тепер, коли я знайомий із пошуковим стилем vim-style search & substitute + regexes. Дякую тонну.
Cory Klein

1
Неправда, Жиль. Дивіться мою відповідь щодо рішення GNU grep.
camh

1
@camh: Ах, я не знав, GNU grep тепер мав повну підтримку PCRE. Я виправив свою відповідь, дякую.
Жиль

1
Ця відповідь особливо корисна для вбудованого Linux, оскільки Busybox grepне підтримує PCRE.
Крейг МакКуін

Очевидно, що існує декілька способів виконання одного й того ж завдання, однак, якщо ОП вимагає використання грепу, чому ви відповідаєте на щось інше? Також ваш перший абзац невірний: так, греп може це зробити.
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

1
+1 для прикладу sed, здається, кращий інструмент для роботи, ніж grep. Один коментар, ^і $є сторонніми, оскільки .*це жадібна відповідність. Однак включення їх може допомогти уточнити наміри регулярного вираження.
Тоні

18

Що ж, якщо ви знаєте, що foobar - це завжди перше слово чи рядок, то ви можете використовувати cut. Так:

grep "foobar" test.file | cut -d" " -f2

-oПеремикач на Grep широко застосовується ( в більшій мірі , ніж розширень Grep Gnu), тим самим grep -o "foobar" test.file | cut -d" " -f2підвищить ефективність цього рішення, яке є більш компактний , ніж з допомогою тверджень щодо попереднього тексту.
сумнівним

Я вважаю, що вам знадобиться grep -o "foobar .*"або grep -o "foobar \w+".
G-Man

9

Якщо PCRE не підтримується, ви можете досягти однакового результату за допомогою двох викликів grep. Наприклад, щоб схопити слово після foobar, зробіть це:

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

Це може бути розширено до довільного слова після foobar, як це (з EREs для читабельності):

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

Вихід:

1

Зверніть увагу, що індекс iнульовий.


6

pcregrepє розумніший -oваріант, який дозволяє вам вибрати групи захоплення, які ви хочете отримати. Отже, використовуючи прикладний файл,

$ pcregrep -o1 "foobar (\w+)" test.txt
bash
happy

4

Використання grepне сумісне між платформами, оскільки -P/ --perl-regexpдоступне лише в GNUgrep , а не BSDgrep .

Ось рішення з використанням ripgrep:

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

Відповідно до man rg:

-r/ --replace REPLACEMENT_TEXTЗамініть кожну відповідність поданим текстом.

Індекси групи захоплення (наприклад, $5) та імена (наприклад, $foo) підтримуються в рядку заміни.

Пов'язане: GH-462 .


2

Я знайшов відповідь @jgshawkey дуже корисною. grepце не такий хороший інструмент для цього, але sed є, хоча тут ми маємо приклад, який використовує grep для захоплення відповідної лінії.

Синтаксис sedge з регулярним виразом sed є ідіосинкратичним, якщо ви до нього не звикли.

Ось ще один приклад: цей аналізує вихід xinput, щоб отримати ціле число ID

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

і я хочу 19

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

Зверніть увагу на синтаксис класу:

[[:digit:]]

і необхідність уникнути наступного +

Я припускаю, що відповідає лише одна лінія.


Це саме те, що я намагався зробити. Дякую!
Джеймс

Трохи простіша версія без зайвих grep, припускаючи, що "TouchPad" знаходиться зліва від "id":echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.