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

У меня есть этот маленький 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'?

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

Ответ 1

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

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
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 does not 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 (просмотр переменной длины) приводит к совпадению предыдущего шаблона, но не включает его в результат. Эквивалент фиксированной длины - (?<=) - шаблон будет включен перед закрывающей скобкой. Вы должны использовать \K, если квантификаторы могут соответствовать строкам разной длины (например, +, *, {2,4}).

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

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

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

Ответ 2

Это действительно невозможно с чистым 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

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

Ответ 4

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

для sed:

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

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

echo "$name.jpg"

Ответ 5

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

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

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

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

Ответ 6

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

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

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

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

Ответ 7

если у вас есть 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