Bash Find Zeilen beginnend mit String

10

Ich habe eine Reihe von Dateien und möchte herausfinden, welche sequentielle Zeilen enthält, die mit einer bestimmten Zeichenfolge beginnen.

Zum Beispiel für die folgende Datei:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Cyyyyyyyyy
Czzzzzzzzz
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
Ceeeeee

Es gibt mehr als eine Zeile, die mit 'C' beginnt, daher möchte ich, dass diese Datei per Befehl gefunden wird.
Zum Beispiel für die folgende Datei:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Es gibt immer eine Zeile, die mit 'C' beginnt. Ich möchte diese Datei nicht. Ich dachte an ein grepoder ein, sedaber ich weiß nicht genau, wie ich es machen soll. Vielleicht mit einem regulären Ausdruck ^C.*$^Coder so etwas. Irgendeine Idee ?

Jérémie
quelle
CIn Ihrem zweiten Beispiel beginnen zwei Zeilen .
Cuonglm
5
Diese Frage ist unklar. Suchen Sie nach Dateien, bei denen mehr als eine Zeile hintereinander beginnt C?
Graeme
Ja das ist was ich will. Entschuldigen Sie das Missverständnis.
Jérémie
2
@terdon, es sieht so aus, als ob mehrzeilige Suchvorgänge mit -P bis 2.5.4 und danach nicht mehr funktionierten, obwohl ich im Changelog nichts finden kann, was erklären würde, warum.
Stéphane Chazelas
1
@Graeme Vielleicht möchten Sie Ihre Antwort wiederherstellen, siehe Stephanes Kommentar, anscheinend funktioniert es für einige ältere grepVersionen.
Terdon

Antworten:

5

Mit pcregrep:

pcregrep -rMl '^C.*\nC' .

POSIXly:

find . -type f -exec awk '
  FNR==1 {last=0; printed=0; next}
  printed {next}
  /^C/ {if (last) {print FILENAME; printed=1; nextfile} else last=1; next}
  {last=0}' {} +

(Dies bedeutet jedoch, dass alle Dateien mit den awknicht unterstützten Implementierungen vollständig gelesen werden müssen nextfile.)


Mit Versionen von GNU grepbis 2.5.4:

grep -rlP '^C.*\nC' .

scheint zu funktionieren, aber es ist ein Zufall und es ist nicht garantiert, dass es funktioniert.

Bevor es in 2.6 (durch dieses Commit ) behoben wurde , hatte GNU grepübersehen, dass die von ihm verwendete PCRE-Suchfunktion für den gesamten aktuell verarbeiteten Puffer übereinstimmen würde grep, was zu allerlei überraschendem Verhalten führte. Zum Beispiel:

grep -P 'a\s*b'

würde mit einer Datei übereinstimmen, die enthält:

bla
bla

Dies würde passen:

printf '1\n2\n' | grep -P '1\n2'

Aber dieses:

(printf '1\n'; sleep 1; printf '2\n') | grep -P '1\n2'

Oder:

(yes | head -c 32766; printf '1\n2\n') > file; grep -P '1\n2' file

würde nicht (da das 1\n2\nüber zwei Puffer von verarbeitet wird grep).

Dieses Verhalten wurde jedoch dokumentiert:

15- Wie kann ich über Linien hinweg übereinstimmen?

Standard grep kann dies nicht, da es grundsätzlich zeilenbasiert ist. Daher entspricht die bloße Verwendung der Zeichenklasse '[: space:]' den Zeilenumbrüchen nicht in der erwarteten Weise. Wenn Ihr grep jedoch mit aktivierten Perl-Mustern kompiliert ist, kann der Modifikator "Perl" verwendet werden (wodurch "." Mit Zeilenumbrüchen übereinstimmt):

     printf 'foo\nbar\n' | grep -P '(?s)foo.*?bar'

Nachdem es in 2.6 behoben wurde, wurde die Dokumentation nicht geändert (ich habe es dort einmal gemeldet ).

Stéphane Chazelas
quelle
Gibt es einen Grund, nicht exitund -exec \;statt nextfile zu verwenden?
Terdon
@terdon, das würde bedeuten, eine awkpro Datei auszuführen . Sie möchten dies nur tun, wenn awkdies nicht unterstützt wird nextfileund Sie einen großen Anteil an Dateien haben, die groß sind und am Anfang der Datei übereinstimmende Zeilen aufweisen.
Stéphane Chazelas
Wie wäre es mit dieser Grep-Technik (ich denke mit neueren Versionen von GNU grep), die mehrzeilige Übereinstimmungen ermöglicht, indem die gesamte Datei wie eine einzelne Zeichenfolge aussieht, indem der Zeilenabschluss auf NUL gesetzt wird. Würden Sie wissen, ob es Einschränkungen gibt?
iruvar
1
@ 1_CR, Das würde die gesamte Datei in den Speicher laden, wenn dort kein NUL-Zeichen vorhanden ist und davon ausgegangen wird, dass Zeilen keine NUL-Zeichen enthalten. Beachten Sie auch, dass ältere Versionen von GNU grep (über die das OP verfügt) nicht verwendet -zwerden können -P. Es gibt kein \Nohne -P, Sie müssten es schreiben, $'[\01-\011\013-\0377]'was nur in C-Gebietsschemas funktionieren würde (siehe thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas
@StephaneChazelas, sehr nützliches Detail, danke
iruvar
2

Mit awk:

awk '{if (p ~ /^C/ && $1 ~ /^C/) print; p=$1}' afile.txt

Dadurch wird der Inhalt der Datei gedruckt, wenn aufeinanderfolgende Zeilen mit a beginnen C. Der Ausdruck untersucht (p ~ /^C/ && $1 ~ /^C/)aufeinanderfolgende Zeilen in der Datei und wird als wahr ausgewertet, wenn das erste Zeichen in beiden übereinstimmt C. In diesem Fall wird die Zeile gedruckt.

Um alle Dateien mit einem solchen Muster zu finden, können Sie den obigen awk über einen findBefehl ausführen :

find /your/path -type f -exec awk '{if (p ~ /^C/ && $1 ~ /^C/) {print FILENAME; exit;} p=$1}' {} \;

In diesem Befehl durchläuft das find+ execjede der Dateien und führt eine ähnliche awkFilterung für jede Datei durch und druckt ihren Namen über, FILENAMEwenn der awk-Ausdruck als wahr ausgewertet wird. Um zu vermeiden, FILENAMEdass eine einzelne Datei mit mehreren Übereinstimmungen mehrmals gedruckt exitwird, wird die Anweisung verwendet (danke @terdon).

mkc
quelle
Meine Frage war nicht klar genug, ich möchte den Namen der Dateien mit mehr als einer aufeinanderfolgenden Zeile wissen, beginnend mitC
Jérémie
@ Jérémie Ich habe meine Antwort aktualisiert.
mkc
Könnten Sie bitte eine Erklärung hinzufügen, wie dies funktioniert? Es besteht auch keine Notwendigkeit flag, nur exitstattdessen. Auf diese Weise müssen Sie die Dateien nicht weiter verarbeiten, nachdem eine Übereinstimmung gefunden wurde.
Terdon
2

Noch eine Option mit GNU sed:

Für eine einzelne Datei:

sed -n -- '/^C/{n;/^C/q 1}' "$file" || printf '%s\n' "$file"

(obwohl es auch die Dateien meldet, die es nicht lesen kann).

Für find:

find . -type f ! -exec sed -n '/^C/{n;/^C/q 1}' {} \; -print

Das Problem mit unlesbaren Dateien, die gedruckt werden, kann durch Schreiben vermieden werden:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
eilen
quelle
Können Sie bitte das detaillieren sed -n '$q1;/^C/{n;/^C/q}'?
Jérémie
Kann mir jemand etwas erklären?
Jérémie
@ Jérémie $q1- zwingt sed, mit einem Fehler zu beenden, wenn kein Muster gefunden wird. Es wird auch mit einem Fehler beendet, wenn etwas mit der Datei nicht stimmt (sie ist nicht lesbar oder defekt). Daher wird es nur dann mit dem Beendigungsstatus 0 beendet, wenn ein Muster gefunden wurde, und es wird an den Druck übergeben. Teil mit /^C/{n;/^C/qist ziemlich einfach. Wenn eine Zeichenfolge gefunden wird, die mit C beginnt, wird die nächste Zeile gelesen, und wenn sie auch mit C beginnt, wird sie mit dem Beendigungsstatus Null beendet.
Eile
1

Angenommen, Ihre Dateien sind klein genug, um in den Speicher eingelesen zu werden:

perl -000ne 'print "$ARGV\n" if /^C[^\n]*\nC/sm' *

Erläuterung:

  • - 000: \n\nAls Datensatztrennzeichen festgelegt, wird der Absatzmodus aktiviert, in dem Absätze (durch aufeinanderfolgende Zeilenumbrüche getrennt) als einzelne Zeilen behandelt werden.
  • -ne: Wenden Sie das als Argument angegebene Skript -eauf jede Zeile der Eingabedatei (en) an.
  • $ARGV : ist die Datei, die gerade verarbeitet wird
  • /^C[^\n]*\nC/: Übereinstimmung Cam Zeilenanfang (siehe die Beschreibung der smModifikatoren unten, warum dies hier funktioniert), gefolgt von 0 oder mehr Nicht-Zeilenumbruchszeichen, einer Zeilenumbruch und einem weiteren C. Mit anderen Worten, finden Sie aufeinanderfolgende Zeilen, die mit beginnen C. * //sm: Diese Übereinstimmungsmodifikatoren sind (wie [hier] dokumentiert):

    • m : Behandle den String als mehrere Zeilen. Das heißt, ändern Sie "^" und "$" so, dass sie nur am linken und rechten Ende der Zeichenfolge mit dem Anfang oder Ende der Zeile übereinstimmen und an einer beliebigen Stelle innerhalb der Zeichenfolge übereinstimmen.

    • s : Behandle den String als einzelne Zeile. Das heißt, ändern Sie "." um irgendeinen Charakter zu finden, sogar eine neue Zeile, die normalerweise nicht passt.

Sie könnten auch etwas Hässliches tun wie:

for f in *; do perl -pe 's/\n/%%/' "$f" | grep -q 'C[^%]*%%C' && echo "$f"; done

Hier perlersetzt der Code Zeilenumbrüche durch %%. Vorausgesetzt, Sie haben keine %%in Ihrer Eingabedatei (groß, wenn natürlich), grepstimmen die Zeilen mit aufeinanderfolgenden Zeilen überein, beginnend mit C.

terdon
quelle
1

LÖSUNG:

( set -- *files ; for f ; do (
set -- $(printf %c\  `cat <$f`)
while [ $# -ge 1 ] ;do [ -z "${1#"$2"}" ] && {
    echo "$f"; break ; } || shift
done ) ; done )

DEMO:

Zuerst erstellen wir eine Testbasis:

abc="a b c d e f g h i j k l m n o p q r s t u v w x y z" 
for l in $abc ; do { i=$((i+1)) h= c= ;
    [ $((i%3)) -eq 0 ] && c="$l" h="${abc%"$l"*}"
    line="$(printf '%s ' $h $c ${abc#"$h"})"
    printf "%s$(printf %s $line)\n" $line >|/tmp/file${i}
} ; done

Das obige erstellt 26 Dateien in /tmpnamens file1-26. In jeder Datei gibt es 27 oder 28 Zeilen, die mit den Buchstaben beginnen a-zund vom Rest des Alphabets gefolgt werden. Jede dritte Datei enthält zwei aufeinanderfolgende Zeilen, in denen das erste Zeichen dupliziert wird.

STICHPROBE:

cat /tmp/file12
...
aabcdefghijkllmnopqrstuvwxyz
babcdefghijkllmnopqrstuvwxyz
cabcdefghijkllmnopqrstuvwxyz
...
kabcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
mabcdefghijkllmnopqrstuvwxyz
...

Und wenn ich mich ändere:

set -- *files

zu:

set -- /tmp/file[0-9]*

Ich bekomme...

AUSGABE:

/tmp/file12
/tmp/file15
/tmp/file18
/tmp/file21
/tmp/file24
/tmp/file3
/tmp/file6
/tmp/file9

Kurz gesagt, die Lösung funktioniert folgendermaßen:

sets Unterschalenpositionen für alle Ihre Dateien und für jede

sets Die Positionen einer verschachtelten Unterschale zum ersten Buchstaben jeder Zeile in jeder Datei während der Schleife.

[ tests ]Wenn $1negiert, $2zeigt dies eine Übereinstimmung an, und wenn ja

echoesder Dateiname dann breaks die aktuelle Schleifeniteration

sonst shifts zur nächsten Einzelzeichenposition, um es erneut zu versuchen

mikeserv
quelle
0

Dieses Skript verwendet grepund cutzum Abrufen von Zeilennummern übereinstimmender Zeilen und sucht nach zwei aufeinander folgenden Nummern. Für die Datei wird ein gültiger Dateiname angenommen, der als erstes Argument an das Skript übergeben wird:

#!/bin/bash

checkfile () {
 echo checking $1
 grep -n -E "^C.*$" $1 | cut -d: -f1 | while read linenum
     do
        : $[ ++PRV ] 
        if [ $linenum == $PRV ]; then return 1; fi
        PRV=$linenum
     done
     return 0
}

PRV="-1"
checkfile $1
if [ $? == 0 ]; then
   echo Consecutive matching lines found in file $1
fi
Michael Martinez
quelle