Przechwytywanie grup z Grep RegEx


380

Mam ten mały skrypt w sh(Mac OSX 10.6) do przeglądania szeregu plików. Google przestało być w tym momencie pomocne:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

Do tej pory (oczywiście dla ciebie guru powłoki) $namezawiera tylko 0, 1 lub 2, w zależności od greptego, czy nazwa pliku odpowiada podanej materii. Chciałbym uchwycić to, co jest w parens ([a-z]+)i zapisać to w zmiennej .

Chciałbym użyć greptylko, jeśli to możliwe . Jeśli nie, proszę, nie używaj Pythona, Perla itp. sedLub coś podobnego - jestem nowy w powłoce i chciałbym zaatakować to z purystycznego * nix.

Ponadto, jako super fajne bonusy , jestem ciekawy, jak mogę połączyć strunę w skorupce? Czy grupa, którą przechwyciłem, to ciąg „nazwa” przechowywany w $ name, a chciałem dodać ciąg „.jpg” na końcu, prawda cat $name '.jpg'?

Wyjaśnij, co się dzieje, jeśli masz czas.


30
Czy grep jest naprawdę czystszym unixem niż sed?
martin clayton

3
Ach, nie chciałem tego sugerować. Miałem tylko nadzieję, że uda się znaleźć rozwiązanie za pomocą narzędzia, którego konkretnie próbuję się tutaj nauczyć. Jeśli nie można rozwiązać za pomocą grep, sedbyłoby świetnie, jeśli można rozwiązać za pomocą sed.
Izaak,

2
Powinienem był umieścić :) na tym btw ...
martin clayton

Hmm, mój mózg jest dzisiaj zbyt smażony, haha.
Izaak,

2
@martinclayton To byłby interesujący argument. Naprawdę uważam, że sed (a dokładniej ed) byłby starszy (a zatem czystszy? Może?) Unix, ponieważ grep wywodzi swoją nazwę od wyrażenia ed g (lobal) / re (gular expression) / p (rint).
sfledgling

Odpowiedzi:


499

Jeśli używasz Bash, nie musisz nawet używać 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

Lepiej umieścić regex w zmiennej. Niektóre wzory nie będą działać, jeśli zostaną dosłownie uwzględnione.

Wykorzystuje =~to, który jest operatorem dopasowania wyrażenia regularnego Basha. Wyniki dopasowania są zapisywane w tablicy o nazwie $BASH_REMATCH. Pierwsza grupa przechwytywania jest przechowywana w indeksie 1, druga (jeśli istnieje) w indeksie 2 itd. Indeks zero jest pełnym dopasowaniem.

Należy pamiętać, że bez kotwic, to wyrażenie regularne (i ten używający grep) będzie pasować do dowolnego z następujących przykładów i nie tylko, które mogą nie być tym, czego szukasz:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Aby wyeliminować drugi i czwarty przykład, wykonaj wyrażenie regularne w ten sposób:

^[0-9]+_([a-z]+)_[0-9a-z]*

co oznacza, że ​​ciąg musi zaczynać się od jednej lub więcej cyfr. Karat reprezentuje początek łańcucha. Jeśli dodasz znak dolara na końcu wyrażenia regularnego, w ten sposób:

^[0-9]+_([a-z]+)_[0-9a-z]*$

wtedy trzeci przykład również zostanie wyeliminowany, ponieważ kropka nie znajduje się wśród znaków w wyrażeniu regularnym, a znak dolara reprezentuje koniec łańcucha. Zauważ, że czwarty przykład również nie pasuje do tego dopasowania.

Jeśli masz GNU grep(około 2,5 lub później, myślę, że kiedy \Koperator został dodany):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

\KUruchamiający (o zmiennej długości wygląd opóźnieniem) powoduje, że powyższy wzór do meczu, ale nie obejmuje dopasowanie w wyniku. Odpowiednikiem o stałej długości jest (?<=)- wzorzec zostanie dołączony przed nawiasem zamykającym. Należy użyć \K, jeśli kwantyfikatory mogą dopasować ciągi o różnej długości (na przykład +, *, {2,4}).

Te (?=)mecze operatora stałej lub zmiennej długości wzory i nazywa się „look-ahead”. Nie zawiera również dopasowanego ciągu w wyniku.

Aby uczynić dopasowanie bez rozróżniania wielkości liter, (?i)używany jest operator. Wpływa na wzorce, które za nim podążają, więc jego pozycja jest znacząca.

Wyrażenie regularne może wymagać dostosowania w zależności od tego, czy w nazwie pliku znajdują się inne znaki. Zauważysz, że w tym przypadku pokazuję przykład konkatenacji łańcucha w tym samym czasie, gdy przechwytywany jest podciąg.


48
W tej odpowiedzi chcę uprościć konkretną linię, która mówi: „Lepiej jest umieścić regex w zmiennej. Niektóre wzorce nie będą działać, jeśli zostaną dosłownie uwzględnione”.
Brandin

5
@FrancescoFrassinelli: Przykładem jest wzór zawierający białe znaki. Ucieczka jest niezręczna i nie można używać cudzysłowów, ponieważ wymusza to wyrażenie regularne na wyrażenie regularne. Prawidłowym sposobem na to jest użycie zmiennej. Podczas zadania można używać cytatów, co znacznie upraszcza.
Wstrzymano do odwołania.

5
/Kskały operatora.
razz

2
@Brandon: To działa. Jakiej wersji Bash używasz? Pokaż mi, że to, co robisz, nie działa, a może powiem ci, dlaczego.
Wstrzymano do odwołania.

2
@mdelolmo: Moja odpowiedź zawiera informacje na temat grep. Został również zaakceptowany przez PO i dość mocno głosowany. Dzięki za recenzję.
Wstrzymano do odwołania.

145

Nie jest to tak naprawdę możliwe w przypadku czystego grep, przynajmniej ogólnie.

Ale jeśli twój wzorzec jest odpowiedni, możesz być w stanie użyć grepwiele razy w potoku, aby najpierw zmniejszyć linię do znanego formatu, a następnie wyodrębnić tylko żądany bit. (Chociaż narzędzia takie jak cuti sedsą w tym znacznie lepsze).

Załóżmy dla argumentu, że twój wzór był nieco prostszy: [0-9]+_([a-z]+)_Możesz to wyodrębnić w następujący sposób:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

Pierwszy grepusunie wszystkie wiersze, które nie pasują do twojego ogólnego patern, drugi grep(który --only-matchingokreślił) wyświetli część alfa nazwy. Działa to tylko dlatego, że wzór jest odpowiedni: „część alfa” jest wystarczająco specyficzna, aby wyciągnąć to, co chcesz.

(Na bok: osobiście użyłbym grep+, cutaby osiągnąć to, czego szukasz :. echo $name | grep {pattern} | cut -d _ -f 2Spowoduje cutto przeanalizowanie linii do pól przez podział na separator _i zwróci tylko pole 2 (numery pól zaczynają się od 1)).

Filozofią Uniksa jest posiadanie narzędzi, które wykonują jedną rzecz i robią to dobrze, i łączą je, aby osiągnąć niebanalne zadania, więc twierdzę, że grep+ sedetc to bardziej uniksowy sposób robienia rzeczy :-)


3
for f in $files; do name=echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | cut -d _ -f 2 ;Aha!
Izaak,

2
nie zgadzam się z tą „filozofią”. jeśli możesz użyć wbudowanych możliwości powłoki bez wywoływania zewnętrznych poleceń, wtedy twój skrypt będzie znacznie szybszy. niektóre narzędzia nakładają się na siebie w funkcji. np. grep i sed i awk. wszystkie z nich wykonują manipulacje sznurkami, ale awk wyróżnia się ponad wszystkie, ponieważ może zrobić znacznie więcej. Praktycznie wszystkie te łańcuchy poleceń, takie jak powyższe podwójne grep lub grep + sed, można skrócić, wykonując je jednym procesem awk.
ghostdog74,

7
@ ghostdog74: Nie ma tutaj argumentu, że łączenie wielu drobnych operacji razem jest ogólnie mniej wydajne niż robienie tego wszystkiego w jednym miejscu, ale podtrzymuję moje twierdzenie, że filozofia uniksowa to wiele narzędzi współpracujących ze sobą. Na przykład tar po prostu archiwizuje pliki, nie kompresuje ich, a ponieważ domyślnie wyświetla dane wyjściowe do STDOUT, możesz przesyłać je przez sieć za pomocą netcat lub kompresować za pomocą bzip2 itp. Co moim zdaniem wzmacnia konwencję i ogólne etos, że narzędzia uniksowe powinny być w stanie współpracować w potokach.
RobM,

krój jest niesamowity - dzięki za podpowiedź! Jeśli chodzi o argument dotyczący narzędzi a efektywność, podoba mi się prostota łączenia łańcuchów.
ether_joe

rekwizyty dla opcji grep's o, która jest bardzo pomocna
chiliNUT

96

Zdaję sobie sprawę, że odpowiedź na to pytanie została już zaakceptowana, ale z „ściśle * purystycznego punktu widzenia” wydaje się, że właściwym narzędziem jest praca pcregrep, o której jeszcze nie wspomniano. Spróbuj zmienić linie:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

do następujących:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

aby uzyskać tylko zawartość grupy przechwytywania 1.

pcregrepNarzędzie wykorzystuje wszystkie z tej samej składni już używane z grep, ale implementuje funkcjonalność, czego potrzebujesz.

Ten parametr -odziała tak jak grepwersja, jeśli jest pusty, ale akceptuje również parametr numeryczny pcregrep, który wskazuje, którą grupę przechwytywania chcesz pokazać.

Dzięki temu rozwiązaniu w skrypcie jest wymagane minimum zmian. Po prostu zamieniasz jedno narzędzie modułowe na inne i dostosowujesz parametry.

Interesująca uwaga: Możesz użyć wielu argumentów -o, aby zwrócić wiele grup przechwytywania w kolejności, w jakiej pojawiają się w wierszu.


3
pcregrepnie jest domyślnie dostępny, w Mac OS Xktórym używa PO
grebneke

4
pcregrepWydaje mi się, że nie rozumiem cyfry po -o: „Nieznana litera opcji„ 1 ”w„ -o1 ”. Nie wspomina się też o tej funkcji, patrząc napcregrep --help
Peter Herdenborg

1
@ WAF przepraszam, chyba powinienem zawrzeć te informacje w moim komentarzu. Jestem na CentOS 6.5, a wersja pcregrep jest najwyraźniej bardzo stara: 7.8 2008-09-05.
Peter Herdenborg

2
tak, bardzo pomoc, np.echo 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei

5
pcregrep8.41 (zainstalowany z apt-get install pcregrepwłączonym Ubuntu 16.03) nie rozpoznaje -Eiprzełącznika. Bez niego jednak działa idealnie. Na macOS, z pcregrepzainstalowanym przez homebrew(także 8.41), jak wspomniano powyżej @anishpatel, przynajmniej w High Sierra -Eprzełącznik nie jest rozpoznawany.
Ville,

27

Uważam, że nie jest to możliwe tylko w grep

dla sed:

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

Zrobię dźgnięcie premii:

echo "$name.jpg"

2
Niestety to sedrozwiązanie nie działa. Po prostu drukuje wszystko w moim katalogu.
Izaak,

zaktualizowany, wyświetli pusty wiersz, jeśli nie ma dopasowania, więc sprawdź to
cobbal

Teraz wyświetla tylko puste linie!
Izaak,

ten sed ma problem. Pierwsza grupa przechwytywania nawiasów obejmuje wszystko. Oczywiście \ 2 nie będzie miał nic.
ghostdog74,

działało w przypadku kilku prostych przypadków testowych ... \ 2 dostaje wewnętrzną grupę
cobbal

16

To rozwiązanie wykorzystuje gawk. Muszę często korzystać z tego, więc stworzyłem dla niego funkcję

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

użyć po prostu zrób

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

Świetny pomysł, ale wydaje się, że nie działa ze spacjami w wyrażeniu regularnym - należy je zastąpić \s. Czy wiesz jak to naprawić?
Adam Ryczkowski

4

Sugestia dla Ciebie - możesz użyć rozszerzenia parametrów, aby usunąć część nazwy od ostatniego podkreślenia i podobnie na początku:

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

Wtedy namebędzie miał wartość abc.

Zobacz dokumenty programistów Apple , wyszukaj „Rozszerzenie parametrów”.


to nie sprawdza ([az] +).
ghostdog74,

@levislevis - to prawda, ale jak skomentował PO, robi to, co było potrzebne.
martin clayton

2

jeśli masz bash, możesz użyć rozszerzonego globowania

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

lub

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

To wygląda intrygująco. Czy mógłbyś dodać do tego małe wyjaśnienie? Lub, jeśli masz takie skłonności, link do szczególnie wnikliwego zasobu, który to wyjaśnia? Dzięki!
Izaak,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.