Захват групп из 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