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

380

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

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

Кроме того, как супер крутые бонусы , мне интересно, как я могу объединить строку в оболочке? Была ли захваченная группа строкой «somename», хранящейся в $ name, и я хотел добавить строку «.jpg» в конец, не так ли cat $name '.jpg'?

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

Исаак
источник
30
Является Grep действительно чище , чем UNIX СЭД?
Мартин Клейтон
3
Ах, не хотел это предлагать. Я просто надеялся, что решение может быть найдено с помощью инструмента, который я специально пытаюсь изучить здесь. Если не возможно решить с помощью grep, то sedбыло бы здорово, если бы можно было решить с помощью sed.
Исаак
3
Я должен был поставить :) на этом кстати ...
Мартин Клейтон
Пш, мой мозг сегодня слишком жарен, ха-ха.
Исаак
2
@martinclayton Это был бы интересный аргумент. Я действительно думаю, что sed (или ed, если быть точным) будет более старым (и, следовательно, более чистым? Может быть?) Unix, потому что grep получает свое имя из выражения ed g (lobal) / re (gular) / p (rint).
13

Ответы:

500

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

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

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

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

Приостановлено до дальнейшего уведомления.
источник
48
В этом ответе я хочу высказать конкретную строку, которая говорит: «Лучше поместить регулярное выражение в переменную. Некоторые шаблоны не будут работать, если они будут включены буквально».
Брандин
5
@FrancescoFrassinelli: пример - шаблон, включающий пробелы. Неловко убегать, и вы не можете использовать кавычки, так как это приводит к регулярному выражению. Правильный способ сделать это - использовать переменную. Кавычки можно использовать во время задания, что значительно упрощает задачу.
Приостановлено до дальнейшего уведомления.
5
/Kоператор качается.
Разз
2
@ Брэндон: это работает. Какую версию Bash вы используете? Покажите мне, что вы делаете, это не работает, и, возможно, я могу сказать вам, почему.
Приостановлено до дальнейшего уведомления.
2
@mdelolmo: Мой ответ включает информацию о grep. Он был также принят ФП и проголосовал довольно много. Спасибо за отрицание.
Приостановлено до дальнейшего уведомления.
145

Это не возможно с чистым 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+ sedetc - это более Unixy способ делать вещи :-)

RobM
источник
3
for f in $files; do name=echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | cut -d _ -f 2 ;Ага!
Исаак
2
я не согласен с этой «философией». если вы можете использовать встроенные возможности оболочки без вызова внешних команд, то ваш скрипт будет работать намного быстрее. Есть некоторые инструменты, которые пересекаются в функции. например, grep, sed и awk. все они выполняют строковые манипуляции, но awk выделяется над всеми ними, потому что он может сделать намного больше. Практически все эти цепочки команд, такие как описанные выше двойные greps или grep + sed, можно сократить, выполнив их одним процессом awk.
ghostdog74
7
@ ghostdog74: Здесь нет аргументов, что объединение множества крошечных операций в целом менее эффективно, чем выполнение всего этого в одном месте, но я придерживаюсь своего утверждения, что философия Unix - это множество инструментов, работающих вместе. Например, tar просто архивирует файлы, но не сжимает их, и, поскольку по умолчанию он выводит в STDOUT, вы можете передавать его по сети с помощью netcat или сжимать с помощью bzip2 и т. Д. Что, на мой взгляд, подкрепляет соглашение и общее Этос, что инструменты Unix должны уметь работать вместе в конвейерах.
RobM
крой потрясающий - спасибо за совет! Что касается инструментов против аргумента эффективности, мне нравится простота объединения инструментов.
ether_joe
поддерживает
97

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

Джон Шервуд
источник
3
pcregrepпо умолчанию недоступен, в Mac OS Xкотором используется OP
grebneke
4
Моя, pcregrepкажется, не понимает цифру после -o: «Неизвестная опция буква« 1 »в« -o1 ». Также нет упоминания об этой функциональности при взглядеpcregrep --help
Питер Херденборг
1
@WAF извините, я должен был включить эту информацию в свой комментарий. Я на Centos 6.5 и версия pcregrep по- видимому , очень старый: 7.8 2008-09-05.
Питер Херденборг
2
да, очень помогите, напримерecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei
5
pcregrep8,41 (устанавливается с apt-get install pcregrepна Ubuntu 16.03) не распознает -Eiпереключатель. Это работает отлично без этого, все же. В macOS с pcregrepустановленным через homebrew(также 8.41), как упоминалось выше @anishpatel, по крайней мере, в High Sierra -Eкоммутатор также не распознается.
Вилле
27

Не возможно в просто grep я верю

для седа:

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

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

echo "$name.jpg"
cobbal
источник
2
К сожалению, это sedрешение не работает. Он просто распечатывает все в моем каталоге.
Исаак
обновленный, будет выводить пустую строку, если не
найдено
Теперь он выводит только пустые строки!
Исаак
у этого седа есть проблема. Первая группа захвата скобок охватывает все. Конечно \ 2 не будет ничего.
ghostdog74
это сработало для некоторых простых тестовых случаев ... \ 2 получает внутреннюю группу
cobbal
16

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

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

использовать просто сделать

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

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

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

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

См. Документацию Apple для разработчиков , ищите «Расширение параметров».

Мартин Клейтон
источник
это не будет проверять ([az] +).
ghostdog74
@levislevis - это правда, но, как прокомментировал ФП, он делает то, что было нужно.
Мартин Клейтон
2

если у вас есть 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 : $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
ghostdog74
источник
Это выглядит интригующим. Не могли бы вы добавить к этому небольшое объяснение? Или, если вы так склонны, ссылку на особенно проницательный ресурс, который это объясняет? Спасибо!
Исаак