529 Stimmen

Erfassen von Gruppen aus einem Grep RegEx

Ich habe dieses kleine Skript in sh (Mac OSX 10.6), um eine Reihe von Dateien zu durchsuchen. Google ist an diesem Punkt 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

Bis jetzt (für euch Shell-Gurus natürlich) $name ist lediglich 0, 1 oder 2, je nachdem, ob grep festgestellt, dass der Dateiname mit der angegebenen Sache übereinstimmt. Ich möchte das erfassen, was innerhalb der Klammern steht ([a-z]+) und speichern diese in einer Variablen .

Ich würde gerne zu verwenden grep nur, wenn möglich . Wenn nicht, dann bitte kein Python oder Perl, etc. sed oder etwas Ähnliches - ich bin neu in der Shell und möchte dies aus dem Blickwinkel des *Nix-Puristen angehen.

Auch als supercooles Bonu s, ich bin neugierig, wie ich String in Shell verketten kann? Ist die Gruppe, die ich erfasst habe, die Zeichenkette "somename", die in $name gespeichert ist, und ich wollte die Zeichenkette ".jpg" am Ende hinzufügen, könnte ich cat $name '.jpg' ?

Bitte erklären Sie, was hier vor sich geht, wenn Sie die Zeit haben.

698voto

Dennis Williamson Punkte 322329

Wenn Sie die Bash verwenden, müssen Sie nicht einmal den Befehl 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, die Regex in eine Variable zu setzen. Einige Muster funktionieren nicht, wenn sie wörtlich enthalten sind.

Dabei werden =~ was der Regex-Match-Operator der Bash ist. Die Ergebnisse der Übereinstimmung werden in einem Array namens $BASH_REMATCH . Die erste Fanggruppe wird in Index 1 gespeichert, die zweite (falls vorhanden) in Index 2 usw. Index Null ist die vollständige Übereinstimmung.

Sie sollten sich darüber im Klaren sein, dass diese Regex (und die Regex, die grep ) passt zu jedem der folgenden und weiteren Beispiele, die vielleicht 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 Ihre Regex wie folgt:

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

die besagt, dass die Zeichenkette Start mit einer oder mehreren Ziffern. Das Karat steht am Anfang der Zeichenfolge. Wenn Sie am Ende der Regex ein Dollar-Zeichen hinzufügen, etwa so:

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

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

Wenn Sie GNU grep (etwa 2,5 oder später, glaube ich, wenn die \K Operator wurde hinzugefügt):

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

El \K Operator (Look-Behind-Operator mit variabler Länge) bewirkt, dass das vorangehende Muster übereinstimmt, aber die Übereinstimmung nicht in das Ergebnis aufgenommen wird. Die Entsprechung mit fester Länge ist (?<=) - würde das Muster vor der schließenden Klammer eingefügt werden. Sie müssen \K wenn Quantifizierer mit unterschiedlich langen Zeichenfolgen übereinstimmen können (z. B. + , * , {2,4} ).

El (?=) Operator findet Muster mit fester oder variabler Länge und wird "Vorausschau" genannt. Außerdem wird die übereinstimmende Zeichenfolge nicht in das Ergebnis aufgenommen.

Um die Übereinstimmung unabhängig von der Groß- und Kleinschreibung zu machen, wird die (?i) Operator verwendet wird. Er wirkt sich auf die Muster aus, die ihm folgen, daher ist seine Position von Bedeutung.

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

195voto

RobM Punkte 7755

Dies ist bei einer reinen Anwendung nicht wirklich möglich. grep zumindest nicht generell.

Wenn Ihr Muster jedoch geeignet ist, können Sie möglicherweise Folgendes verwenden grep mehrfach innerhalb einer Pipeline verwenden, um Ihre Zeile zunächst auf ein bekanntes Format zu reduzieren und dann nur das gewünschte Bit zu extrahieren. (Obwohl Werkzeuge wie cut y sed sind in dieser Hinsicht weitaus besser).

Nehmen wir einmal an, dass Ihr Muster etwas einfacher ist: [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 grep würde alle Zeilen entfernen, die nicht zu Ihrem Gesamtmuster passen, die zweite grep (die eine --only-matching angegeben) würde den Alpha-Teil des Namens anzeigen. Dies funktioniert nur, weil das Muster geeignet ist: "alpha portion" ist spezifisch genug, um das Gewünschte herauszufinden.

(Nebenbei: Ich persönlich würde die grep + cut um das zu erreichen, was Sie wollen: echo $name | grep {pattern} | cut -d _ -f 2 . Dies wird cut um die Zeile in Felder zu zerlegen, indem das Trennzeichen geteilt wird _ und gibt nur Feld 2 zurück (die Feldnummern beginnen bei 1).

Die Unix-Philosophie ist es, Werkzeuge zu haben, die eine Sache tun, und zwar gut, und sie zu kombinieren, um nicht-triviale Aufgaben zu bewältigen, also würde ich argumentieren, dass grep + sed etc ist eine eher unixige Art, Dinge zu tun :-)

128voto

John Sherwood Punkte 1269

Mir ist klar, dass bereits eine Antwort auf diese Frage akzeptiert wurde, aber aus der Sicht eines "strikten *nix-Puristen" scheint es, dass das richtige Werkzeug für diese Aufgabe folgendes ist pcregrep die bisher 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 den folgenden:

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

um nur den Inhalt der Erfassungsgruppe 1 zu erhalten.

El pcregrep verwendet dieselbe Syntax, die Sie bereits mit grep , sondern implementiert die von Ihnen benötigte Funktionalität.

Der Parameter -o funktioniert genauso wie der grep Version, wenn sie leer ist, aber sie akzeptiert auch einen numerischen Parameter in pcregrep , die angibt, welche Erfassungsgruppe Sie anzeigen möchten.

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

Interessante Anmerkung: Sie können mehrere -o Argumente verwenden, um mehrere Erfassungsgruppen in der Reihenfolge zurückzugeben, in der sie in der Zeile erscheinen.

44voto

cobbal Punkte 68319

Mit grep allein ist das nicht möglich, glaube ich.

für sed:

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

Ich werde aber einen Versuch mit dem Bonus wagen:

echo "$name.jpg"

27voto

chirag nayak Punkte 300
str="1w 2d 1h"
regex="([0-9])w ([0-9])d ([0-9])h"
if [[ $str =~ $regex ]]
then
    week="${BASH_REMATCH[1]}"
    day="${BASH_REMATCH[2]}"
    hour="${BASH_REMATCH[3]}"
    echo $week --- $day ---- $hour
fi

Ausgabe: 1 --- 2 ---- 1

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X