Захоплення груп із 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чи щось подібне - я новачок у оболонці і хотів би атакувати це під кутом * nix purist.

Крім того, мені, як супер класному бонусу , цікаво, як я можу об'єднати рядок в оболонку? Чи є група, яку я захопила, рядок "somename", що зберігається в $ name, і я хотів додати рядок ".jpg" до кінця, чи можу я cat $name '.jpg'?

Поясніть, будь ласка, що відбувається, якщо у вас є час.


30
Чи справді grep чистіший Unix, ніж sed?
— Мартін Клейтон

3
Ах, не мав на увазі цього. Я просто сподівався, що рішення можна знайти за допомогою інструменту, який я спеціально намагаюся тут вивчити. Якщо неможливо вирішити використання grep, то sedбуло б чудово, якщо це можливо вирішити за допомогою sed.
— Ісаак

2
Я мусив би поставити :) на це btw ...
— martin clayton

Psh, мій мозок сьогодні занадто смажений ха-ха.
— Ісаак

2
@martinclayton Це був би цікавий аргумент. Я дійсно думаю, що sed, (або ед, якщо бути точним) був би старшим (і, отже, чистішим? Можливо?) Unix, тому що grep походить це ім'я від виразу ed g (lobal) / re (gular вираз) / p (rint).
— ffledgling

Відповіді:


499

Якщо ви використовуєте 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_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оператор гірських порід.
— razz

2
@Brandon: Це працює. Яку версію 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+ sedтощо - це більш універсальний спосіб робити речі :-)


3
for f in $files; do name=відлуння $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | вирізати -d _ -f 2 ;Ага!
— Ісаак

2
Я не згоден з цією "філософією". якщо ви можете використовувати оболонки у вбудованих можливостях без виклику зовнішніх команд, ваш сценарій буде набагато швидшим у виконанні. є деякі інструменти, які перекриваються у функції. наприклад, grep і sed і awk. всі вони проводять стринг-маніпуляції, але awk виділяється вище всіх, тому що це може зробити набагато більше. Практично, всі ті ланцюжки команд, як вищезгадані подвійні клавіші чи grep + sed, можна скоротити, виконуючи їх одним процесом awk.
— ghostdog74

7
@ ghostdog74: Тут немає жодного аргументу, що пов’язувати багато крихітних операцій разом, як правило, менш ефективно, ніж робити це все в одному місці, але я відстоюю своє твердження, що філософія Unix - це багато інструментів, які працюють разом. Наприклад, tar просто архівує файли, він не стискає їх, і оскільки він виводить на STDOUT за замовчуванням, ви можете передавати його по всій мережі за допомогою netcat або стискати з bzip2 тощо. Що, на мій погляд, підсилює конвенцію та загальне Ethos, що інструменти Unix повинні мати можливість спільно працювати в трубах.
— RobM

розріз приголомшливий - дякую за пораду! Що стосується аргументу проти ефективності, то мені подобається простота ланцюжкових інструментів.
— ether_joe

реквізит для опції grep, що дуже корисно
— chiliНУ

96

Я усвідомлюю, що відповідь на це вже прийнято, але з "строго * нікс-пуристського кута" здається, що правильним інструментом для роботи є 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якому використовується ОП
— 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

Неможливо просто в греп я вважаю

для sed:

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

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

echo "$name.jpg"

2
На жаль, це sedрішення не працює. Він просто роздруковує все в моєму каталозі.
— Ісаак

оновлено, буде виведено порожній рядок, якщо немає відповідності, тому обов'язково перевірте це
— кобаль

Тепер він видає лише порожні рядки!
— Ісаак

ця седа має проблему. Перша група захоплення дужок охоплює все. Звичайно \ 2 нічого не матиме.
— ghostdog74

він працював для деяких простих тестових випадків ... \ 2 отримує внутрішню групу
— кабал

16

Це рішення, яке використовує gawk. Мені здається, що мені потрібно часто користуватися, тому я створив для нього функцію

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

використовувати просто робити

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

Відмінна ідея, але, схоже, не працює з пробілами в регулярному вираженні - їх потрібно замінити \s. Ви знаєте, як це виправити?
— Адам Річковський

4

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

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

Тоді nameматиме значення abc.

Див. Документи для розробників Apple , шукайте вперед "Розширення параметрів".


це не перевірятиметься на ([az] +).
— ghostdog74

@levislevis - це правда, але, як коментує ОП, він робить все, що потрібно.
— Мартін Клейтон

2

якщо у вас баш, ви можете використовувати розширений глобус

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

Це виглядає інтригуюче. Чи можете ви додати трохи пояснення до цього? Або, якщо ви настільки схильні, посилання на особливо проникливий ресурс, який пояснює це? Дякую!
— Ісаак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.