Захват групп из регулярного выражения Grep

у меня есть этот маленький скрипт в 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, в зависимости от if grep обнаружил, что имя файла соответствует указанному вопросу. что я хотел бы, чтобы захватить то, что внутри parens ([a-z]+) и сохраните это в переменной.

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

также, как супер-круто бонуs, Мне любопытно, как я могу объединить строку в оболочке? Группа, которую я захватил, была строкой "somename", хранящейся в $name, и я хотел добавить строку ".jpg " до конца, не мог бы я cat $name '.jpg'?

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

7 ответов


если вы используете 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. Результаты совпадения сохраняются в массиве $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 оператор (переменная длина look-behind) вызывает совпадение предыдущего шаблона, но не включает совпадение в результат. Этот фиксированной длины, эквивалентной (?<=) - схемы будет включен перед закрывающей скобкой. Вы должны использовать \K если кванторы могут соответствовать строкам разной длины (например,+, *, {2,4}).

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

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

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


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

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

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

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

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

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

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


Я понимаю, что ответ уже был принят для этого, но с "строго *Nix purist angle" кажется, что правильным инструментом для работы является 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, что указывает, какую группу захвата вы хотите показать.

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

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


невозможно только в grep я считаю

для sed:

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

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

echo "$name.jpg"

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

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

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

$ echo 'hello world' | regex1 'hello\s(.*)'
world

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

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

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

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


Если у вас есть bash, вы можете использовать расширенный globbing

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 : "
done

или

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : "
done