Erfassen von Gruppen aus einem Grep RegEx


380

Ich habe dieses kleine Skript in sh(Mac OSX 10.6), um eine Reihe von Dateien zu durchsuchen. Google ist an dieser Stelle nicht mehr hilfreich:

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

Bisher (für Sie Shell-Gurus offensichtlich) gilt $namelediglich 0, 1 oder 2, je nachdem, ob grepfestgestellt wurde, dass der Dateiname mit der angegebenen Angelegenheit übereinstimmt. Ich möchte erfassen, was sich in den Parens befindet, ([a-z]+)und dies in einer Variablen speichern .

Ich möchte verwenden , grepnur, wenn möglich . Wenn nicht, bitte kein Python oder Perl usw. sedoder ähnliches - ich bin neu in der Shell und möchte dies aus puristischer Sicht angreifen.

Als supercooler Bonu bin ich auch gespannt, wie ich einen String in der Shell verketten kann. Ist die Gruppe, die ich erfasst habe, die in $ name gespeicherte Zeichenfolge "somename", und ich wollte die Zeichenfolge ".jpg" am Ende hinzufügen, oder cat $name '.jpg'?

Bitte erklären Sie, was los ist, wenn Sie Zeit haben.


30
Ist grep wirklich reiner als sed?
Martin Clayton

3
Ah, wollte das nicht vorschlagen. Ich hatte nur gehofft, dass eine Lösung mit einem Tool gefunden werden kann, das ich hier speziell lernen möchte. Wenn es nicht möglich ist, mit zu lösen grep, sedwäre es großartig, wenn es möglich ist, mit zu lösen sed.
Isaac

2
Ich hätte ein :) auf das übrigens setzen sollen ...
Martin Clayton

Psh, mein Gehirn ist heute viel zu frittiert, haha.
Isaac

2
@martinclayton Das wäre ein interessantes Argument. Ich denke wirklich, dass sed (oder ed um genau zu sein) älter (und daher reiner? Vielleicht?) Unix wäre, weil grep seinen Namen vom ed Ausdruck g (lobal) / re (gular Ausdruck) / p (rint) ableitet.
Jungvogel

Antworten:


500

Wenn Sie Bash verwenden, müssen Sie nicht einmal Folgendes verwenden 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

Es ist besser, den regulären Ausdruck in eine Variable zu setzen. Einige Muster funktionieren nicht, wenn sie wörtlich enthalten sind.

Dies verwendet =~den Regex-Match-Operator von Bash. Die Ergebnisse der Übereinstimmung werden in einem aufgerufenen Array gespeichert $BASH_REMATCH. Die erste Erfassungsgruppe wird in Index 1 gespeichert, die zweite (falls vorhanden) in Index 2 usw. Index Null ist die vollständige Übereinstimmung.

Sie sollten sich bewusst sein, dass dieser Regex (und der verwendete grep) ohne Anker mit einem der folgenden Beispiele und mehr übereinstimmt, die möglicherweise nicht das sind, wonach Sie suchen:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Um das zweite und vierte Beispiel zu eliminieren, machen Sie Ihren regulären Ausdruck wie folgt:

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

was sagt der Zeichenfolge muss beginnen mit einem oder mehreren Ziffern. Das Karat repräsentiert den Anfang der Saite. Wenn Sie am Ende der Regex ein Dollarzeichen hinzufügen, gehen Sie wie folgt vor:

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

dann wird auch das dritte Beispiel entfernt, da der Punkt nicht zu den Zeichen in der Regex gehört und das Dollarzeichen das Ende der Zeichenfolge darstellt. Beachten Sie, dass das vierte Beispiel auch diese Übereinstimmung nicht besteht.

Wenn Sie GNU haben grep(ungefähr 2,5 oder höher, denke ich, als der \KOperator hinzugefügt wurde):

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

Der \KOperator (Look-Behind mit variabler Länge) bewirkt, dass das vorhergehende Muster übereinstimmt, schließt die Übereinstimmung jedoch nicht in das Ergebnis ein. Das Äquivalent fester Länge ist (?<=)- das Muster würde vor der schließenden Klammer stehen. Sie müssen verwenden , \Kwenn quantifiers können Strings unterschiedlicher Länge (zB übereinstimmen +, *, {2,4}).

Der (?=)Operator stimmt mit Mustern fester oder variabler Länge überein und wird als "Vorausschau" bezeichnet. Die übereinstimmende Zeichenfolge ist auch nicht im Ergebnis enthalten.

Um die Übereinstimmung zwischen Groß- und Kleinschreibung zu unterscheiden, wird der (?i)Operator verwendet. Es beeinflusst die Muster, die ihm folgen, so dass seine Position signifikant ist.

Die Regex muss möglicherweise angepasst werden, je nachdem, ob der Dateiname andere Zeichen enthält. Sie werden feststellen, dass in diesem Fall ein Beispiel für die Verkettung einer Zeichenfolge zur gleichen Zeit gezeigt wird, zu der der Teilstring erfasst wird.


48
In dieser Antwort möchte ich die spezifische Zeile mit der Aufschrift "Es ist besser, den regulären Ausdruck in eine Variable einzufügen. Einige Muster funktionieren nicht, wenn sie wörtlich enthalten sind."
Brandin

5
@FrancescoFrassinelli: Ein Beispiel ist ein Muster, das Leerzeichen enthält. Es ist umständlich zu entkommen, und Sie können keine Anführungszeichen verwenden, da dies den Übergang von einem regulären Ausdruck zu einem normalen String erzwingt. Der richtige Weg, dies zu tun, ist die Verwendung einer Variablen. Während der Aufgabe können Anführungszeichen verwendet werden, um die Arbeit zu vereinfachen.
Bis auf weiteres angehalten.

5
/KOperator rockt.
Razz

2
@Brandon: Es funktioniert. Welche Version von Bash verwenden Sie? Zeig mir, was du tust, das funktioniert nicht und vielleicht kann ich dir sagen warum.
Bis auf weiteres angehalten.

2
@mdelolmo: Meine Antwort enthält Informationen über grep. Es wurde auch vom OP akzeptiert und ziemlich positiv bewertet. Danke für die Ablehnung.
Bis auf weiteres angehalten.

145

Dies ist mit rein nicht wirklich möglich grep, zumindest nicht allgemein.

Wenn Ihr Muster jedoch geeignet ist, können Sie es möglicherweise grepmehrmals innerhalb einer Pipeline verwenden, um zuerst Ihre Zeile auf ein bekanntes Format zu reduzieren und dann genau das gewünschte Bit zu extrahieren. (Obwohl Werkzeuge dies mögen cutund sedweitaus besser sind).

Nehmen wir zum Zwecke der Argumentation an, dass Ihr Muster etwas einfacher war: [0-9]+_([a-z]+)_Sie könnten dies wie folgt extrahieren:

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

Die erste grepwürde alle Zeilen entfernen, die nicht zu Ihrem Gesamtmuster passen, die zweite grep(die --only-matchingangegeben wurde) würde den Alpha-Teil des Namens anzeigen. Dies funktioniert nur, weil das Muster geeignet ist: "Alpha-Teil" ist spezifisch genug, um das herauszuholen, was Sie wollen.

(Nebenbei: Persönlich würde ich grep+ verwenden cut, um das zu erreichen, wonach Sie suchen : echo $name | grep {pattern} | cut -d _ -f 2. Dadurch wird cutdie Zeile durch Aufteilen auf das Trennzeichen in Felder analysiert _und nur Feld 2 zurückgegeben (Feldnummern beginnen bei 1)).

Die Unix-Philosophie besteht darin, Tools zu haben, die eine Sache gut machen und sie kombinieren, um nicht triviale Aufgaben zu erfüllen. Ich würde also argumentieren, dass grep+ sedetc eine unixyere Art ist, Dinge zu tun :-)


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

2
Ich bin mit dieser "Philosophie" nicht einverstanden. Wenn Sie die integrierten Funktionen der Shell verwenden können, ohne externe Befehle aufzurufen, ist die Leistung Ihres Skripts viel schneller. Es gibt einige Werkzeuge, deren Funktion sich überschneidet. zB grep und sed und awk. Alle machen String-Manipulationen, aber awk hebt sich von allen ab, weil es viel mehr kann. Praktisch können alle Verkettungen von Befehlen, wie die oben genannten Double Greps oder Grep + Sed, verkürzt werden, indem sie mit einem awk-Prozess ausgeführt werden.
Ghostdog74

7
@ ghostdog74: Kein Argument hier, dass das Verketten vieler kleiner Operationen im Allgemeinen weniger effizient ist als das alles an einem Ort, aber ich stehe zu meiner Behauptung, dass die Unix-Philosophie darin besteht, dass viele Tools zusammenarbeiten. Zum Beispiel archiviert tar nur Dateien, komprimiert sie nicht, und da es standardmäßig an STDOUT ausgegeben wird, können Sie es mit netcat über das Netzwerk leiten oder mit bzip2 usw. komprimieren, was meiner Meinung nach die Konvention und das Allgemeine verstärkt Ethos, dass Unix-Tools in der Lage sein sollten, in Pipes zusammenzuarbeiten.
RobM

Schnitt ist super - danke für den Tipp! Was das Argument Werkzeuge gegen Effizienz betrifft, mag ich die Einfachheit der Verkettung von Werkzeugen.
ether_joe

Requisiten für die Option o von grep, das ist sehr hilfreich
chiliNUT

96

Mir ist klar, dass eine Antwort darauf bereits akzeptiert wurde, aber aus einem "streng puristischen Blickwinkel" scheint es das richtige Werkzeug für den Job zu sein pcregrep, das noch nicht erwähnt worden zu sein scheint. Versuchen Sie, die Zeilen zu ändern:

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

Zu dem Folgendem:

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

um nur den Inhalt der Erfassungsgruppe zu erhalten 1.

Das pcregrepTool verwendet dieselbe Syntax, die Sie bereits verwendet haben grep, implementiert jedoch die Funktionen, die Sie benötigen.

Der Parameter -ofunktioniert genau wie die grepVersion, wenn er leer ist, akzeptiert jedoch auch einen numerischen Parameter in pcregrep, der angibt, welche Erfassungsgruppe Sie anzeigen möchten.

Mit dieser Lösung ist ein Minimum an Änderungen im Skript erforderlich. Sie ersetzen einfach ein modulares Dienstprogramm durch ein anderes und optimieren die Parameter.

Interessanter Hinweis: Sie können mehrere -o-Argumente verwenden, um mehrere Erfassungsgruppen in der Reihenfolge zurückzugeben, in der sie in der Zeile angezeigt werden.


3
pcregrepist standardmäßig nicht verfügbar, Mac OS Xwas das OP verwendet
grebneke

4
Mein pcregrepscheint die Ziffer nach dem -o"Unbekannten Optionsbuchstaben '1' in" -o1 " nicht zu verstehen . Auch diese Funktion wird beim Betrachten nicht erwähntpcregrep --help
Peter Herdenborg

1
@ WAF Entschuldigung, ich denke, ich hätte diese Informationen in meinen Kommentar aufnehmen sollen. Ich bin auf Centos 6.5 und die pcregrep-Version ist anscheinend sehr alt : 7.8 2008-09-05.
Peter Herdenborg

2
Ja, sehr hilfreich, zBecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei

5
pcregrep8.41 (installiert mit apt-get install pcregrepon Ubuntu 16.03) erkennt den -EiSwitch nicht. Ohne funktioniert es jedoch perfekt. Unter macOS, pcregrepdas über homebrew(ebenfalls 8.41) installiert wurde, wie oben in @anishpatel erwähnt, wird der -ESwitch zumindest unter High Sierra ebenfalls nicht erkannt.
Ville

27

Ich glaube nicht nur in grep möglich

für sed:

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

Ich werde den Bonus allerdings ausprobieren:

echo "$name.jpg"

2
Leider sedfunktioniert diese Lösung nicht. Es druckt einfach alles in meinem Verzeichnis aus.
Isaac

aktualisiert, gibt eine leere Zeile aus, wenn es keine Übereinstimmung gibt, also überprüfen Sie dies
cobbal

Es werden nur noch Leerzeilen ausgegeben!
Isaac

Dieses Sed hat ein Problem. Die erste Gruppe der Klammererfassung umfasst alles. Natürlich wird \ 2 nichts haben.
Ghostdog74

es hat für einige einfache Testfälle funktioniert ... \ 2 bekommt die innere Gruppe
Cobbal

16

Dies ist eine Lösung, die Gawk verwendet. Es ist etwas, das ich oft verwenden muss, also habe ich eine Funktion dafür erstellt

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

einfach zu benutzen

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

Tolle Idee, scheint aber nicht mit Leerzeichen im regulären Ausdruck zu funktionieren - sie müssen durch ersetzt werden \s. Wissen Sie, wie Sie das Problem beheben können?
Adam Ryczkowski

4

Ein Vorschlag für Sie - Sie können die Parametererweiterung verwenden, um den Teil des Namens ab dem letzten Unterstrich zu entfernen, und zwar zu Beginn:

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

Dann namewird der Wert haben abc.

Suchen Sie in den Apple- Entwicklerdokumenten nach "Parametererweiterung".


Dies wird nicht nach ([az] +) suchen.
Ghostdog74

@levislevis - das stimmt, aber wie vom OP kommentiert, macht es das, was nötig war.
Martin Clayton

2

Wenn Sie Bash haben, können Sie Extended Globbing verwenden

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

oder

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

Das sieht faszinierend aus. Könnten Sie vielleicht eine kleine Erklärung hinzufügen? Oder, wenn Sie dazu neigen, auf eine besonders aufschlussreiche Ressource verlinken, die dies erklärt? Vielen Dank!
Isaac
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.