Захват групп из Grep RegEx

250

У меня есть этот маленький script в sh (Mac OSX 10.6), чтобы просмотреть массив файлов. На этом этапе Google перестает быть полезной:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

До сих пор (очевидно, для вас, гуру-оболочки) $name просто содержит 0, 1 или 2, в зависимости от того, было ли grep установлено, что имя файла соответствует предоставленному вопросу. Я хотел бы захватить то, что внутри parens ([a-z]+), и сохранить его переменной.

Я бы хотел, чтобы использовал grep, если возможно. Если нет, пожалуйста, не используйте Python или Perl и т.д. sed или что-то в этом роде - я новичок в оболочке и хотел бы атаковать это от угла пуриста * nix.

Кроме того, как super-cool bonu, мне любопытно, как я могу объединить строку в оболочке? Я захватил группу, это строка "somename", хранящаяся в $name, и я хотел добавить строку ".jpg" до конца, могу ли я cat $name '.jpg'?

Пожалуйста, объясните, что происходит, если у вас есть время.

  • 23
    Является Grep действительно чище , чем UNIX СЭД?
  • 1
    Ах, не хотел это предлагать. Я просто надеялся, что решение можно будет найти с помощью инструмента, который я специально пытаюсь изучить здесь. Если это не удается решить с помощью grep , то sed было бы здорово, если бы это можно решить с помощью sed .
Показать ещё 3 комментария
Теги:
grep

7 ответов

307
Лучший ответ

Если вы используете Bash, вам даже не нужно использовать grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

Лучше поместить регулярное выражение в переменную. Некоторые шаблоны не будут работать, если они будут включены буквально.

В этом случае используется =~, который является оператором соответствия Bash regex. Результаты совпадения сохраняются в массив с именем $BASH_REMATCH. Первая группа захвата хранится в индексе 1, вторая (если есть) в индексе 2 и т.д. Индексный ноль - полное совпадение.

Вы должны знать, что без привязок это регулярное выражение (и одно с использованием grep) будет соответствовать любому из следующих примеров и более, что может и не быть тем, что вы ищете:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Чтобы устранить второй и четвертый примеры, сделайте свое регулярное выражение следующим образом:

^[0-9]+_([a-z]+)_[0-9a-z]*

в котором говорится, что строка должна начинаться с одной или нескольких цифр. Карат представляет собой начало струны. Если вы добавите знак доллара в конце регулярного выражения, например:

^[0-9]+_([a-z]+)_[0-9a-z]*$

то третий пример также будет устранен, поскольку точка не входит в число символов в регулярном выражении, а знак доллара представляет конец строки. Обратите внимание, что четвертый пример также не соответствует этому совпадению.

Если у вас есть GNU grep (около 2.5 или новее, я думаю, когда был добавлен оператор \K):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

Оператор \K (внешний вид с переменной длиной) приводит к совпадению предыдущего шаблона, но не включает совпадение в результате. Эквивалент фиксированной длины (?<=) - шаблон будет включен перед закрывающей скобкой. Вы должны использовать \K, если квантификаторы могут соответствовать строкам разной длины (например, +, *, {2,4}).

Оператор (?=) соответствует шаблонам с фиксированной или переменной длиной и называется "look-ahead". Он также не включает в себя согласованную строку в результате.

Чтобы сделать совпадение без учета регистра, используется оператор (?i). Это влияет на шаблоны, которые следуют за ним, поэтому его положение является значительным.

Возможно, потребуется изменить регулярное выражение в зависимости от наличия других символов в имени файла. Вы заметите, что в этом случае я показываю пример объединения строки одновременно с захватом подстроки.

  • 30
    В этом ответе я хочу высказать конкретную строку, которая гласит: «Лучше поместить регулярное выражение в переменную. Некоторые шаблоны не будут работать, если они включены буквально».
  • 0
    «Лучше поместить регулярное выражение в переменную. Некоторые шаблоны не будут работать, если включены буквально». - Почему это происходит? Есть ли способ их исправить?
Показать ещё 22 комментария
112

Это действительно невозможно с чистым grep, по крайней мере, в общем случае.

Но если ваш шаблон подходит, вы можете использовать grep несколько раз в конвейере, чтобы сначала сократить свою линию до известного формата, а затем извлечь только тот бит, который вы хотите. (Хотя такие инструменты, как cut и sed, намного лучше).

Предположим ради аргумента, что ваш шаблон был немного проще: [0-9]+_([a-z]+)_ Вы можете извлечь это так:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

Первый grep удалит любые строки, которые не совпадают с вашим общим patern, второй grep (который имеет --only-matching указанный) отобразит альфа-часть имени. Это работает только потому, что шаблон подходит: "альфа-часть" достаточно конкретна, чтобы вытащить то, что вы хотите.

(Помимо этого: Лично я использовал бы grep + cut для достижения того, что вам нужно: echo $name | grep {pattern} | cut -d _ -f 2. Это получает cut для разбора строки в полях путем разделения на разделитель _ и возвращает только поле 2 (номера полей начинаются с 1)).

Unix-философия состоит в том, чтобы иметь инструменты, которые делают что-то одно, и делают это хорошо, и объединяют их для достижения нетривиальных задач, поэтому я бы сказал, что grep + sed и т.д. - это еще один способ Unixy вещи: -)

  • 3
    for f in $files; do name= echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | cut -d _ -f 2 ; Ага!
  • 1
    используя оболочку, нет необходимости в grep + cut. тратить накладные расходы, если OP имеет много файлов ..
Показать ещё 4 комментария
66

Я понимаю, что ответ уже был принят для этого, но из "строгого" пуристского угла "кажется, что правильным инструментом для задания является pcregrep, что не кажется как уже упоминалось. Попробуйте изменить строки:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

к следующему:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

чтобы получить только содержимое группы захвата 1.

В инструменте pcregrep используется все тот же синтаксис, который вы уже использовали с grep, но реализуете необходимые функции.

Параметр -o работает так же, как версия grep, если он голый, но также принимает числовой параметр в pcregrep, который указывает, какую группу захвата вы хотите отобразить.

При таком решении в script требуется минимальное изменение. Вы просто заменяете одну модульную утилиту другой и настраиваете параметры.

Интересное примечание:. Вы можете использовать несколько аргументов -o для возврата нескольких групп захвата в том порядке, в котором они отображаются в строке.

  • 3
    pcregrep не доступен по умолчанию в Mac OS X что используется OP
  • 1
    +1 за один лайнер
Показать ещё 7 комментариев
18

Невозможно только в grep Я верю

для sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

Я возьму удар в бонус, хотя:

echo "$name.jpg"
  • 0
    Ах, конечно, спасибо за это, ха-ха.
  • 2
    К сожалению, что sed решение не работает. Он просто распечатывает все в моем каталоге.
Показать ещё 5 комментариев
7

Это решение, использующее gawk. Это то, что я нахожу, мне нужно часто использовать, поэтому я создал для него функцию

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

использовать только do

$ echo 'hello world' | regex1 'hello\s(.*)'
world
  • 0
    Отличная идея, но, похоже, не работает с пробелами в регулярном выражении - их нужно заменить на \s . Вы знаете, как это исправить?
1

если у вас есть bash, вы можете использовать расширенное подтягивание

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

или

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done
  • 0
    Это выглядит интригующим. Не могли бы вы добавить небольшое объяснение? Или, если вы так склонны, ссылка на особенно проницательный ресурс, который объясняет это? Спасибо!
  • 0
    справочное руководство bash - 3.5.8.1 Pattern Matching
Показать ещё 1 комментарий
1

Предложение для вас - вы можете использовать расширение параметра, чтобы удалить часть имени с последнего подчеркивания вперед и аналогично в начале:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Тогда name будет иметь значение abc.

См. Apple документы разработчика, выполните поиск вперед для "Расширения параметров".

  • 1
    Ах, теперь это работает. Но достаточно ли это для Unix ? Хм ...
  • 0
    это не будет проверять ([az] +).
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню