Может ли grep выводить только указанные группы, которые совпадают?

293

Скажи, у меня есть файл:

# 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
для тех, кому не нужен grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
хранилище

Ответы:

328

GNU grep имеет -Pопцию для регулярных выражений в стиле Perl и -oвозможность печатать только то, что соответствует шаблону. Их можно объединить с помощью проверочных утверждений (описанных в разделе « Расширенные шаблоны» на man-странице perlre ), чтобы удалить часть шаблона grep из того, что определено для соответствия -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
CAMH
источник
3
Как вы это сделаете, если ваше регулярное выражение имеет больше, чем группировка? (как следует из названия?)
barracel
4
@ barracel: Я не верю, что ты можешь. Время дляsed(1)
camh
1
@camh Я только что проверил, что grep -oP 'foobar \K\w+' test.txtничего не дает с OP test.txt. Версия grep 2.5.1. Что может быть не так? O_O
SOUser
@XichenLi: не могу сказать. Я только что собрал v2.5.1 из grep (он довольно старый - с 2006 года), и он работал для меня.
Camh
@ SOUser: я испытал то же самое - ничего не выводит в файл. Я отправил запрос на редактирование, чтобы включить «>» перед именем файла для отправки вывода, так как это сработало для меня.
rjchicago
39

Стандартный grep не может этого сделать, но последние версии 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. Благодаря тонну.
Кори Кляйн
1
Неверно, Жиль. Смотрите мой ответ для решения GNU grep.
Camh
1
@camh: Ах, я не знал, что GNU grep теперь полностью поддерживает PCRE. Я исправил свой ответ, спасибо.
Жиль
1
Этот ответ особенно полезен для встроенного Linux, так как Busybox grepне имеет поддержки PCRE.
Крейг МакКуин
Очевидно, что существует несколько способов выполнить одну и ту же задачу, однако, если OP запрашивает использование grep, почему вы отвечаете на что-то еще? Кроме того, ваш первый абзац неверен: да, grep может это сделать.
fcm
33
    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
источник
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повысит эффективность этого решения, которое является более компактен , чем с помощью утверждений касательно предшествующего текста.
dubiousjim
Я считаю, что вам нужно grep -o "foobar .*"или grep -o "foobar \w+".
G-Man
9

Если PCRE не поддерживается, вы можете достичь того же результата с помощью двух вызовов grep. Например, чтобы взять слово после foobar, сделайте так:

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

Это может быть расширено до произвольного слова после foobar следующим образом (с ERE для удобства чтения):

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

Выход:

1

Обратите внимание, что индекс начинается с iнуля.

Тор
источник
6

pcregrepимеет более умную -oопцию, которая позволяет вам выбрать, какие группы захвата вы хотите выводить. Итак, используя ваш файл примера,

$ pcregrep -o1 "foobar (\w+)" test.txt
bash
happy
G-Man
источник
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 .

kenorb
источник
2

Я нашел ответ @jgshawkey очень полезным. grepэто не очень хороший инструмент для этого, но sed есть, хотя здесь у нас есть пример, который использует grep для захвата соответствующей строки.

Синтаксис регулярного выражения sed уникален, если вы к нему не привыкли.

Вот еще один пример: этот анализирует вывод xinput, чтобы получить целое число идентификатора

⎜   ↳ 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"
Амит Найду