Захоплення груп із 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.