bash - Überprüfen Sie, ob in einer Datei mehrere Zeichenfolgen oder reguläre Ausdrücke vorhanden sind




search grep (14)

Ich möchte überprüfen, ob alle meine Zeichenfolgen in einer Textdatei vorhanden sind. Sie können in derselben Zeile oder in verschiedenen Zeilen vorhanden sein. Und teilweise Übereinstimmungen sollten in Ordnung sein. So was:

...
string1
...
string2
...
string3
...
string1 string2
...
string1 string2 string3
...
string3 string1 string2
...
string2 string3
... and so on

Im obigen Beispiel könnten wir Regexes anstelle von Strings haben.

Der folgende code überprüft beispielsweise, ob eine meiner Zeichenfolgen in der Datei vorhanden ist:

if grep -EFq "string1|string2|string3" file; then
  # there is at least one match
fi

Wie überprüfe ich, ob alle vorhanden sind? Da wir nur an der Anwesenheit aller Übereinstimmungen interessiert sind, sollten wir das Lesen der Datei beenden, sobald alle Zeichenfolgen übereinstimmen.

Ist es möglich, dies zu tun, ohne grep mehrmals aufrufen zu müssen (was sich nicht skalieren lässt, wenn die Eingabedatei groß ist oder wenn eine große Anzahl von Zeichenfolgen übereinstimmt) oder ein Tool wie awk oder python ?

Gibt es auch eine Lösung für Zeichenfolgen, die leicht für reguläre Ausdrücke erweitert werden können?


git grep

Hier ist die Syntax mit git grep mit mehreren Mustern:

git grep --all-match --no-index -l -e string1 -e string2 -e string3 file

Sie können Muster auch mit Booleschen Ausdrücken wie --and , --or und --not .

Überprüfe man git-grep auf Hilfe.

--all-match Wenn Sie mehrere --all-match angeben, wird dieses Flag angegeben, um die Übereinstimmung auf Dateien zu beschränken, deren Zeilen mit allen übereinstimmen .

--no-index Sucht nach Dateien im aktuellen Verzeichnis, die nicht von Git verwaltet werden.

-l / --files-with-matches / --name-only Zeigt nur die Namen der Dateien an.

-e Der nächste Parameter ist das Muster. Standardmäßig wird Basic Regexp verwendet.

Andere zu berücksichtigende Parameter:

--threads Anzahl der zu verwendenden grep-Worker-Threads.

-q / --quiet / --silent keine übereinstimmenden Zeilen aus. Beenden mit Status 0, wenn eine Übereinstimmung vorliegt.

Um den --basic-regexp zu ändern, können Sie auch -G / --basic-regexp (Standard), -F / --fixed-strings , -E / --extended-regexp , -P / --perl-regexp , -f file und andere.


Angenommen, alle zu überprüfenden Zeichenfolgen befinden sich in einer Datei strings.txt und die Datei, die Sie einchecken möchten, ist input.txt.

Die Antwort wurde basierend auf Kommentaren aktualisiert:

$ diff <( sort -u strings.txt )  <( grep -o -f strings.txt input.txt | sort -u )

Erklärung:

Verwenden Sie die Option -o von grep, um nur die Zeichenfolgen zu finden, an denen Sie interessiert sind. Dadurch werden alle Zeichenfolgen angezeigt, die in der Datei input.txt vorhanden sind. Verwenden Sie dann diff, um die Zeichenfolgen abzurufen, die nicht gefunden werden. Wenn alle Zeichenfolgen gefunden würden, wäre das Ergebnis nichts. Oder überprüfen Sie einfach den Exit-Code von diff.

Was es nicht tut:

  • Beenden Sie, sobald alle Übereinstimmungen gefunden wurden.
  • Erweiterbar auf regx.
  • Überlappende Übereinstimmungen.

Was es macht:

  • Finde alle Übereinstimmungen.
  • Einzelruf zu grep.
  • Verwendet weder awk noch Python.

Das folgende python Skript sollte den Trick ausführen. Es Art macht das Äquivalent nennen grep ( re.search ) mehrmals für jede Zeile - dh es jedes Muster für jede Zeile durchsucht, aber da Sie kein Prozess jedes Mal drauflegen, sollte es viel effizienter sein. Außerdem werden die bereits gefundenen Muster entfernt und angehalten, wenn alle gefunden wurden.

#!/usr/bin/env python

import re

# the file to search
filename = '/path/to/your/file.txt'

# list of patterns -- can be read from a file or command line 
# depending on the count
patterns = [r'py.*$', r'\s+open\s+', r'^import\s+']
patterns = map(re.compile, patterns)

with open(filename) as f:
    for line in f:
        # search for pattern matches
        results = map(lambda x: x.search(line), patterns)

        # remove the patterns that did match
        results = zip(results, patterns)
        results = filter(lambda x: x[0] == None, results)
        patterns = map(lambda x: x[1], results)

        # stop if no more patterns are left
        if len(patterns) == 0:
            break

# print the patterns which were not found
for p in patterns:
    print p.pattern

Sie können eine separate Prüfung für einfache Zeichenfolgen ( string in line ) hinzufügen, wenn Sie mit einfachen (nicht regulären) Zeichenfolgen arbeiten - dies ist etwas effizienter.

Löst das dein Problem?


Eine weitere Perl-Variante: Immer wenn alle angegebenen Zeichenfolgen übereinstimmen. Auch wenn die Datei zur Hälfte gelesen wird, wird die Verarbeitung abgeschlossen und die Ergebnisse werden nur gedruckt

> perl -lne ' /\b(string1|string2|string3)\b/ and $m{$1}++; eof if keys %m == 3; END { print keys %m == 3 ? "Match": "No Match"}'  all_match.txt
Match
> perl -lne ' /\b(string1|string2|stringx)\b/ and $m{$1}++; eof if keys %m == 3; END { print keys %m == 3 ? "Match": "No Match"}'  all_match.txt
No Match

In Python können mit dem Modul fileinput die Dateien in der Befehlszeile angegeben oder der Text zeilenweise von stdin gelesen werden. Sie können die Zeichenfolgen in einer Python-Liste fest codieren.

# Strings to match, must be valid regular expression patterns
# or be escaped when compiled into regex below.
strings = (
    r'string1',
    r'string2',
    r'string3',
)

oder lesen Sie die Zeichenfolgen aus einer anderen Datei

import re
from fileinput import input, filename, nextfile, isfirstline

for line in input():
    if isfirstline():
        regexs = map(re.compile, strings) # new file, reload all strings

    # keep only strings that have not been seen in this file
    regexs = [rx for rx in regexs if not rx.match(line)] 

    if not regexs: # found all strings
        print filename()
        nextfile()

Viele dieser Antworten sind soweit in Ordnung.

Aber wenn Leistung ein Problem ist - sicherlich möglich, wenn die Eingabe groß ist und Sie viele Tausende von Mustern haben -, erhalten Sie eine große Beschleunigung, wenn Sie ein Tool wie lex oder verwenden flex , das einen echten deterministischen endlichen Automaten als Erkenner erzeugt, anstatt aufzurufen ein Regex-Interpreter einmal pro Muster.

Der endliche Automat führt unabhängig von der Anzahl der Muster einige Maschinenbefehle pro eingegebenem Zeichen aus .

Eine schnörkellose Flex-Lösung:

%{
void match(int);
%}
%option noyywrap

%%

"abc"       match(0);
"ABC"       match(1);
[0-9]+      match(2);
/* Continue adding regex and exact string patterns... */

[ \t\n]     /* Do nothing with whitespace. */
.   /* Do nothing with unknown characters. */

%%

// Total number of patterns.
#define N_PATTERNS 3

int n_matches = 0;
int counts[10000];

void match(int n) {
  if (counts[n]++ == 0 && ++n_matches == N_PATTERNS) {
    printf("All matched!\n");
    exit(0);
  }
}

int main(void) {
  yyin = stdin;
  yylex();
  printf("Only matched %d patterns.\n", n_matches);
  return 1;
}

Ein Nachteil ist, dass Sie dies für jeden Satz von Mustern erstellen müssen. Das ist gar nicht so schlecht:

flex matcher.y
gcc -O lex.yy.c -o matcher

Führen Sie es jetzt aus:

./matcher < input.txt

Der einfachste Weg für mich zu überprüfen, ob die Datei alle drei Muster enthält, besteht darin, nur übereinstimmende Muster zu erhalten, nur eindeutige Teile auszugeben und Zeilen zu zählen. Dann können Sie es mit einer einfachen test 3 -eq $grep_lines überprüfen: test 3 -eq $grep_lines .

 grep_lines=$(grep -Eo 'string1|string2|string3' file | uniq | wc -l)

In Bezug auf Ihre zweite Frage denke ich nicht, dass es möglich ist, das Lesen der Datei zu beenden, sobald mehr als ein Muster gefunden wird. Ich habe die Manpage für grep gelesen und es gibt keine Optionen, die Ihnen dabei helfen könnten. Sie können das Lesen von Zeilen nur nach einer bestimmten mit der Option grep -m [number] stoppen, was unabhängig von übereinstimmenden Mustern geschieht.

Ziemlich sicher, dass eine benutzerdefinierte Funktion für diesen Zweck benötigt wird.


Dieses gnu-awk Skript funktioniert möglicherweise:

cat fileSearch.awk
re == "" {
   exit
}
{
   split($0, null, "\\<(" re "\\>)", b)
   for (i=1; i<=length(b); i++)
      gsub("\\<" b[i] "([|]|$)", "", re)
}
END {
   exit (re != "")
}

Dann benutze es als:

if awk -v re='string1|string2|string3' -f fileSearch.awk file; then
   echo "all strings were found"
else
   echo "all strings were not found"
fi

Alternativ können Sie diese gnu grep Lösung mit der PCRE Option verwenden:

grep -qzP '(?s)(?=.*\bstring1\b)(?=.*\bstring2\b)(?=.*\bstring3\b)' file
  • Mit -z wir grep read complete file in eine einzige Zeichenkette.
  • Wir verwenden mehrere Lookahead-Zusicherungen, um zu bestätigen, dass alle Zeichenfolgen in der Datei vorhanden sind.
  • Regex muss (?s) oder den DOTALL Mod verwenden, um eine Übereinstimmung zwischen den Zeilen zu DOTALL .

Wie pro man grep :

-z, --null-data
   Treat  input  and  output  data as sequences of lines, each terminated by a 
   zero byte (the ASCII NUL character) instead of a newline.

Eine rekursive Lösung. Durchlaufen Sie die Dateien nacheinander. Überprüfen Sie für jede Datei, ob sie mit dem ersten Muster übereinstimmt, und brechen Sie vorzeitig ab (-m1: bei erstem Abgleich), nur wenn sie mit dem ersten Muster übereinstimmt, suchen Sie nach dem zweiten Muster und so weiter:

#!/bin/bash

patterns="[email protected]"

fileMatchesAllNames () {
  file=$1
  if [[ $# -eq 1 ]]
  then
    echo "$file"
  else
    shift
    pattern=$1
    shift
    grep -m1 -q "$pattern" "$file" && fileMatchesAllNames "$file" [email protected]
  fi
}

for file in *
do
  test -f "$file" && fileMatchesAllNames "$file" $patterns
done

Verwendungszweck:

./allfilter.sh cat filter java
test.sh

Sucht im aktuellen Verzeichnis nach den Tokens "cat", "filter" und "java". Fand sie nur in "test.sh".

Daher wird grep im schlimmsten Fall häufig aufgerufen (Finden der ersten N-1-Muster in der letzten Zeile jeder Datei, mit Ausnahme des N-ten Musters).

Bei einer informierten Bestellung (selten zuerst, frühe Übereinstimmungen zuerst) sollte die Lösung jedoch schnell zumutbar sein, da viele Dateien vorzeitig abgebrochen werden, weil sie nicht mit dem ersten Keyword übereinstimmen oder vorzeitig akzeptiert wurden, da sie mit einem Keyword in der Nähe übereinstimmen Zum Seitenanfang.

Beispiel: Sie suchen eine Scala-Quelldatei, die tailrec (etwas selten verwendet), mutable (selten verwendet, aber in diesem Fall bei import-Anweisungen ganz oben), main (selten verwendet, oft nicht ganz oben) und println (oft) enthält gebrauchte, unvorhersehbare Position), würden Sie sie bestellen:

./allfilter.sh mutable tailrec main println 

Performance:

ls *.scala | wc 
 89      89    2030

In 89 Scala-Dateien habe ich die Schlüsselwortverteilung:

for keyword in mutable tailrec main println; do grep -m 1 $keyword *.scala | wc -l ; done 
16
34
41
71

Das Durchsuchen mit einer leicht modifizierten Version der Skripte, bei der ein Dateimuster als erstes Argument verwendet werden kann, dauert ungefähr 0,2 Sekunden:

time ./allfilter.sh "*.scala" mutable tailrec main println
Filepattern: *.scala    Patterns: mutable tailrec main println
aoc21-2017-12-22_00:16:21.scala
aoc25.scala
CondenseString.scala
Partition.scala
StringCondense.scala

real    0m0.216s
user    0m0.024s
sys 0m0.028s

in knapp 15.000 Codelines:

cat *.scala | wc 
  14913   81614  610893

aktualisieren:

Nachdem wir die Kommentare zu der Frage gelesen haben, dass wir vielleicht über Tausende von Mustern sprechen, scheint es keine kluge Idee zu sein, sie als Argumente zu behandeln. Lesen Sie sie besser aus einer Datei und übergeben Sie den Dateinamen als Argument - möglicherweise auch für die Liste der zu filternden Dateien:

#!/bin/bash

filelist="$1"
patternfile="$2"
patterns="$(< $patternfile)"

fileMatchesAllNames () {
  file=$1
  if [[ $# -eq 1 ]]
  then
    echo "$file"
  else
    shift
    pattern=$1
    shift
    grep -m1 -q "$pattern" "$file" && fileMatchesAllNames "$file" [email protected]
  fi
}

echo -e "Filepattern: $filepattern\tPatterns: $patterns"
for file in $(< $filelist)
do
  test -f "$file" && fileMatchesAllNames "$file" $patterns
done

Wenn die Anzahl und Länge der Muster / Dateien die Möglichkeiten der Argumentübergabe überschreitet, kann die Liste der Muster in viele Musterdateien aufgeteilt und in einer Schleife verarbeitet werden (z. B. von 20 Musterdateien):

for i in {1..20}
do
   ./allfilter2.sh file.$i.lst pattern.$i.lst > file.$((i+1)).lst
done

Es ist ein interessantes Problem, und auf der grep-Manpage ist nichts ersichtlich, was eine einfache Antwort nahe legt. Möglicherweise gibt es einen verrückten Regex, der dies tun würde, aber mit einer unkomplizierten Kette von Greps ist dies möglicherweise klarer, obwohl die Datei n-mal gescannt wird. Zumindest bei der Option -q wird sie jedes Mal beim ersten Treffer zurückgesetzt, und bei der Auswahl von && wird eine Verknüpfung hergestellt, wenn eine der Zeichenfolgen nicht gefunden wird.

$grep -Fq string1 t && grep -Fq string2 t && grep -Fq string3 t
$echo $?
0

$grep -Fq string1 t && grep -Fq blah t && grep -Fq string3 t
$echo $?
1

Nur aus Gründen der Vollständigkeit der Lösungen können Sie ein anderes Tool verwenden und mehrere Greps und awk / sed oder große (und wahrscheinlich langsame) Shell-Loops vermeiden. Ein solches Tool ist agrep .

agrep ist eigentlich eine Art egrep , das auch die Operation zwischen Mustern unterstützt, indem es verwendet ; als Mustertrennzeichen.

Wie egrep und wie die meisten bekannten Tools ist agrep ein Tool, das Datensätze / Zeilen agrep , und daher brauchen wir immer noch eine Möglichkeit, die gesamte Datei als einen einzelnen Datensatz zu behandeln.
Darüber hinaus bietet -d Option -d , um Ihr benutzerdefiniertes Datensatztrennzeichen festzulegen.

Einige Tests:

$ cat file6
str4
str1
str2
str3
str1 str2
str1 str2 str3
str3 str1 str2
str2 str3

$ agrep -d '$$\n' 'str3;str2;str1;str4' file6;echo $?
str4
str1
str2
str3
str1 str2
str1 str2 str3
str3 str1 str2
str2 str3
0

$ agrep -d '$$\n' 'str3;str2;str1;str4;str5' file6;echo $?
1

$ agrep -p 'str3;str2;str1' file6  #-p prints lines containing all three patterns in any position
str1 str2 str3
str3 str1 str2

Kein Werkzeug ist perfekt, und agrep hat auch einige Einschränkungen; Sie können keine regulären Ausdrücke / Muster verwenden, die länger als 32 Zeichen sind, und einige Optionen stehen bei Verwendung mit regulären Ausdrücken nicht zur Verfügung - all dies wird in der entsprechenden agrep


Vielleicht mit Gnu sed

Katze match_word.sh

sed -z '
  /\b'"$2"'/!bA
  /\b'"$3"'/!bA
  /\b'"$4"'/!bA
  /\b'"$5"'/!bA
  s/.*/0\n/
  q
  :A
  s/.*/1\n/
' "$1"

und du nennst es so:

./match_word.sh infile string1 string2 string3

Gibt 0 zurück, wenn alle Übereinstimmungen gefunden wurden, sonst 1

Hier können Sie nach 4 Saiten suchen

Wenn Sie mehr möchten, können Sie Zeilen wie hinzufügen

/\b'"$x"'/!bA

$ cat allstringsfile | tr '\n' ' ' |  awk -f awkpattern1

Wo allstringsfile ist Ihre Textdatei, wie in der ursprünglichen Frage. awkpattern1 enthält die Zeichenfolgenmuster mit der Bedingung &&:

$ cat awkpattern1
/string1/ && /string2/ && /string3/

perl -lne '%m = (%m, map {$_ => 1} m!\b(string1|string2|string3)\b!g); END { print scalar keys %m == 3 ? "Match": "No Match"}' file




grep