it-swarm.com.de

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 hat an dieser Stelle aufgehört, hilfreich zu sein:

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

Bisher (offensichtlich für Sie Shell-Gurus) enthält $name lediglich 0, 1 oder 2, je nachdem, ob grep festgestellt hat, dass der Dateiname mit der angegebenen Angelegenheit übereinstimmt. Ich möchte erfassen, was sich in den Parens befindet ([a-z]+) und das in einer Variablen speichern.

Ich möchte wenn möglich nur grep verwenden. Wenn nicht, bitte nicht Python oder Perl usw. sed oder so ähnlich - ich bin neu bei Shell und möchte dies aus puristischer * nix-Perspektive angreifen.

Außerdem bin ich als super-cooles Bon neugierig, wie ich eine Zeichenfolge in der Shell verketten kann. Ist die Gruppe, die ich aufgenommen habe, die Zeichenfolge "somename", die in $ name gespeichert ist, und ich wollte die Zeichenfolge ".jpg" am Ende hinzufügen, könnte ich cat $name '.jpg'?

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

343
Isaac

Wenn Sie Bash verwenden, müssen Sie nicht einmal grep verwenden:

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 aufgenommen werden.

Dies verwendet =~, den Regex-Match-Operator von Bash. Die Ergebnisse der Übereinstimmung werden in einem Array mit dem Namen $BASH_REMATCH gespeichert. 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 darüber im Klaren sein, dass dieser reguläre Ausdruck (und der mit grep) ohne Anker mit den folgenden und weiteren Beispielen übereinstimmt, die möglicherweise nicht Ihren Vorstellungen entsprechen:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

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

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

was besagt, dass der String Start mit einer oder mehreren Ziffern sein muss. Das Karat steht für den Anfang der Saite. Wenn Sie am Ende des regulären Ausdrucks ein Dollarzeichen einfügen, gehen Sie wie folgt vor:

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

dann wird auch das dritte Beispiel gestrichen, da der Punkt nicht zu den Zeichen im regulären Ausdruck gehört und das Dollarzeichen das Ende der Zeichenfolge darstellt. Beachten Sie, dass das vierte Beispiel diese Übereinstimmung ebenfalls nicht erfüllt.

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

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

Der \K -Operator (Look-Behind variabler Länge) bewirkt, dass das vorhergehende Muster übereinstimmt, schließt die Übereinstimmung jedoch nicht in das Ergebnis ein. Das Äquivalent mit fester Länge ist (?<=) - das Muster wird vor der schließenden Klammer eingefügt. Sie müssen \K verwenden, wenn Quantifizierer mit Zeichenfolgen unterschiedlicher Länge übereinstimmen dürfen (z. B. +, *, {2,4}).

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

Um die Groß- und Kleinschreibung der Übereinstimmungen zu vermeiden, wird der Operator (?i) verwendet. Es wirkt sich auf die darauf folgenden Muster aus, sodass seine Position von Bedeutung ist.

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

454

Das ist mit pure grep nicht wirklich möglich, zumindest nicht generell.

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

Nehmen wir an, Ihr Muster wäre ein bisschen einfacher: [0-9]+_([a-z]+)_ Sie könnten dies folgendermaßen extrahieren:

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

Das erste grep entfernt alle Zeilen, die nicht zu Ihrem Gesamtmuster passen, das zweite grep (das --only-matching angegeben hat) zeigt den Alpha-Teil des Namens an. Dies funktioniert nur, weil das Muster geeignet ist: "Alpha-Anteil" ist spezifisch genug, um das herauszuholen, was Sie wollen.

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

Die Unix-Philosophie besteht darin, Werkzeuge zu haben, die eines leisten und es gut machen, und sie zu kombinieren, um nicht-triviale Aufgaben zu erfüllen. Daher würde ich argumentieren, dass grep + sed usw. eine eher unixartige Methode ist Sachen machen :-)

132
RobM

Mir ist klar, dass eine Antwort darauf bereits akzeptiert wurde, aber aus "rein puristischer Sicht" scheint das richtige Werkzeug für den Job pcregrep zu sein, was 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 Tool pcregrep verwendet dieselbe Syntax, die Sie bereits mit grep verwendet haben, implementiert jedoch die von Ihnen benötigte Funktionalität.

Der Parameter -o funktioniert genau wie die Version grep, akzeptiert jedoch auch einen numerischen Parameter in pcregrep, der angibt, welche Erfassungsgruppe Sie anzeigen möchten.

Mit dieser Lösung ist nur ein Minimum an Änderungen im Skript erforderlich. Sie ersetzen einfach ein modulares Dienstprogramm durch ein anderes und ändern 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.

87
John Sherwood

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 erstechen:

echo "$name.jpg"
25
cobbal

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'}']}'; }

zu benutzen einfach machen

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

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

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

Dann hat name den Wert abc.

Siehe Apple Entwicklerdokumentation , suchen Sie nach 'Parameter Expansion'.

4
martin clayton

wenn Sie Bash haben, können Sie erweitertes 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
2
ghostdog74