Datei search engine.dart: Unterschied zwischen den Versionen
(23 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
Zeile 9: | Zeile 9: | ||
/// [parser]: delivers the description of the program's options | /// [parser]: delivers the description of the program's options | ||
void usage(String message, {ArgParser parser}) { | void usage(String message, {ArgParser parser}) { | ||
print('''Usage: dgrep [<options>] <pattern> <file1> [<file2> | print('''Usage: dgrep [<options>] <pattern> [<file1> [<file2> ...]] | ||
Searches <pattern> in <file1> <file2>... | Searches <pattern> in <file1> <file2>... | ||
<fileN>: a directory or a shell file pattern like '*.txt' | |||
or both like /home/*.txt | |||
<option>:'''); | <option>:'''); | ||
if (parser == null) { | if (parser == null) { | ||
Zeile 24: | Zeile 26: | ||
* <code>///</code> markiert einen Kommentar, der automatisch für die Dokumentation ausgewertet wird. | * <code>///</code> markiert einen Kommentar, der automatisch für die Dokumentation ausgewertet wird. | ||
* <code>print(parser.usage);</code> Das Attribut <code>usage</code> der Klasse <code>ArgParser</code> gibt eine automatisch erstellte Beschreibung der Optionen aus. | * <code>print(parser.usage);</code> Das Attribut <code>usage</code> der Klasse <code>ArgParser</code> gibt eine automatisch erstellte Beschreibung der Optionen aus. | ||
* Es wird eine mehrzeiliger Stringkonstante benutzt, die mit drei Apostrophen <code>"'Usage:...<option>:'"</code> eingeschlossen ist. Hinweis: drei Apostrophe sind im Wiki nicht darstellbar, daher die "Simulation" mit Gänsefüßchen und Apostroph. | |||
= Klasse ExitException = | = Klasse ExitException = | ||
Diese Klasse wird als Ausnahme benutzt, um aus einer verschachtelten Situation unkompliziert ans Ende des Programms zu gelangen, wenn bestimmte Bedingungen erfüllt sind. | Diese Klasse wird als Ausnahme benutzt, um aus einer verschachtelten Situation unkompliziert ans Ende des Programms zu gelangen, wenn bestimmte Bedingungen erfüllt sind. | ||
<pre>class ExitException { | <pre>/// Used for jump out of nested calls. | ||
class ExitException { | |||
final String reason; | final String reason; | ||
ExitException(this.reason); | ExitException(this.reason); | ||
Zeile 35: | Zeile 39: | ||
= Die Klasse FileOptions = | = Die Klasse FileOptions = | ||
Diese Klasse speichert die Optionen, die sich auf die Dateiauswahl beziehen. | Diese Klasse speichert die Optionen, die sich auf die Dateiauswahl beziehen. | ||
<pre>class FileOptions { | <pre>/// Stores file selection options. | ||
class FileOptions { | |||
bool recursive; | bool recursive; | ||
RegExp excluded; | RegExp excluded; | ||
Zeile 58: | Zeile 63: | ||
** <code>FileOptions()</code> wird nur in Tests benutzt. | ** <code>FileOptions()</code> wird nur in Tests benutzt. | ||
** <code>FileOptions.fromArgument()</code> holt die Optionen aus einem Objekt der Klasse <code>ArgResult</code> aus dem externen Paket <code>args</code>. | ** <code>FileOptions.fromArgument()</code> holt die Optionen aus einem Objekt der Klasse <code>ArgResult</code> aus dem externen Paket <code>args</code>. | ||
* <code>recursive = results['recursive'];</code> Das boolsche Attribut <code>recursive</code> wird direkt aus dem Objekt der Klasse <code>ArgResult</code> übernommen. | * <code>recursive = results['recursive'];</code> Das boolsche Attribut <code>recursive</code> wird direkt aus dem Objekt der Klasse <code>ArgResult</code> übernommen. Wir lernen: Zugriff auf die Optionen erfolgt mittels Namen <code>recursive</code> mit dem eckige-Klammer-Operator wie bei Maps. | ||
= Die Klasse SearchEngine = | = Die Klasse SearchEngine = | ||
Zeile 66: | Zeile 71: | ||
Zuerst werden die Attribute definiert: | Zuerst werden die Attribute definiert: | ||
<pre> | <pre> | ||
/// Searches a regular expression in files. | |||
class SearchEngine { | class SearchEngine { | ||
static bool storeResult = false; | static bool storeResult = false; | ||
Zeile 83: | Zeile 89: | ||
String rc; | String rc; | ||
final lines = <String>[]; | final lines = <String>[]; | ||
final formatPlaceholders = RegExp(r'%[#fpnFehcl1-9]%'); | static final formatPlaceholders = RegExp(r'%[#fpnFehcl1-9]%'); | ||
</pre> | </pre> | ||
* Das Attribut <code>formatPlaceholders</code> ist als <code>static</code> definiert, damit findet die Initialiserung nur einmal statt, nicht bei jedem Objekt. Das ist möglich, weil sich der Ausdruck zur Beschreibung eines Platzhalters im Formatstring nie was ändert. | |||
== Der Konstruktor == | == Der Konstruktor == | ||
Zeile 95: | Zeile 102: | ||
Diese Methode stellt einen String zur Ausgabe zusammen, gesteuert durch ein Format, das per Option definiert wird. | Diese Methode stellt einen String zur Ausgabe zusammen, gesteuert durch ein Format, das per Option definiert wird. | ||
Die notwendigen Daten werden per Parameter übergeben, auch das Format, da es zwei Formate gibt: das für Trefferzeilen und das für Zeilen "der Umgebung" (siehe Option code>--above-lines</code> | Die notwendigen Daten werden per Parameter übergeben, auch das Format, da es zwei Formate gibt: das für Trefferzeilen und das für Zeilen "der Umgebung" (siehe Option <code>--above-lines</code> | ||
oder <code>below-lines</code>). | oder <code>below-lines</code>). | ||
<pre>String formatLine( | <pre>/// Creates an output line depending on a given [format]. | ||
String formatLine( | |||
String format, String file, String line, int lineNo, String prefix, | String format, String file, String line, int lineNo, String prefix, | ||
{RegExpMatch match, int hits}) { | {RegExpMatch match, int hits}) { | ||
Zeile 116: | Zeile 124: | ||
break; | break; | ||
case 'p': | case 'p': | ||
line2 = line2.replaceAll(placeholder, | line2 = line2.replaceAll(placeholder, dirname(file)); | ||
break; | break; | ||
case 'n': | case 'n': | ||
line2 = line2.replaceAll(placeholder, | line2 = line2.replaceAll(placeholder, basename(file)); | ||
break; | break; | ||
case 'F': | case 'F': | ||
line2 = line2.replaceAll( | line2 = line2.replaceAll( | ||
placeholder, | placeholder, basenameWithoutExtension(file)); | ||
break; | break; | ||
case 'e': | case 'e': | ||
line2 = line2.replaceAll(placeholder, | line2 = line2.replaceAll(placeholder, extension(file)); | ||
break; | break; | ||
case 'h': | case 'h': | ||
line2 = line2.replaceAll(placeholder, match?.group(0) ?? ''); | line2 = line2.replaceAll(placeholder, match?.group(0) ?? ''); | ||
break; | break; | ||
case 'c': | |||
line2 = line2.replaceAll(placeholder, hits?.toString() ?? ''); | line2 = line2.replaceAll(placeholder, hits?.toString() ?? ''); | ||
break; | break; | ||
Zeile 162: | Zeile 170: | ||
** <code>for (var match2 in formatPlaceholders.allMatches(format))</code> | ** <code>for (var match2 in formatPlaceholders.allMatches(format))</code> | ||
*** <code>formatPlaceholders</code> ist ein regulärer Ausdruck, der alle Platzhalter im Format beschreibt: <code>RegExp(r'%[#fpnFehcl1-9]%')</code> Zwei Prozentzeichen, zwischen denen eines der aufgeführten Zeichen <code>#</code> ... <code>l</code> oder eine Ziffer <code>1-9</code> liegt. | *** <code>formatPlaceholders</code> ist ein regulärer Ausdruck, der alle Platzhalter im Format beschreibt: <code>RegExp(r'%[#fpnFehcl1-9]%')</code> Zwei Prozentzeichen, zwischen denen eines der aufgeführten Zeichen <code>#</code> ... <code>l</code> oder eine Ziffer <code>1-9</code> liegt. | ||
*** Die Klasse <code>RegExp</code> liefert mit der Methode <code>allMatches()<code> nacheinander alle Treffer des regulären Ausdrucks in dem als Parameter übergebenen String (hier <code>format</code>) liefert. Über diese Treffer wird die Schleife gebildet. | *** Die Klasse <code>RegExp</code> liefert mit der Methode <code>allMatches()</code> nacheinander alle Treffer des regulären Ausdrucks in dem als Parameter übergebenen String (hier <code>format</code>) liefert. Über diese Treffer wird die Schleife gebildet. | ||
** <code>final placeholder = match2.group(0);</code> Der Treffer (beispielsweise <code>%f%</code> wird in der Variablen <code>placeholder</code> gemerkt. | ** <code>final placeholder = match2.group(0);</code> Der Treffer (beispielsweise <code>%f%</code>) wird in der Variablen <code>placeholder</code> gemerkt. | ||
** <code>switch(placeholder[1])</code> Es wird das zweite Zeichen (gezählt wird ab 0) als Fallunterscheidung genommen: | ** <code>switch(placeholder[1])</code> Es wird das zweite Zeichen (gezählt wird ab 0) als Fallunterscheidung genommen: | ||
*** <code>case '#':</code> Ist dieses 2.te Zeichen ein <code>#</code> ... | *** <code>case '#':</code> Ist dieses 2.te Zeichen ein <code>#</code> ... | ||
*** <code>line2 = line2.replaceAll(placeholder, lineNo.toString());</code> werden alle diese Platzhalter (<code>%#%</code> durch die Zeilenummer, gewandelt in einen String, ersetzt. | *** <code>line2 = line2.replaceAll(placeholder, lineNo.toString());</code> Es werden alle diese Platzhalter (<code>%#%</code>) durch die Zeilenummer, gewandelt in einen String, ersetzt. | ||
** Nach diesem Schema werden die auch die anderen Platzhalter ersetzt. | ** Nach diesem Schema werden die auch die anderen Platzhalter ersetzt. | ||
** <code> | ** <code>match?.group(0) ?? "</code> Der Operator <code>?.</code> sorgt dafür, dass kein Fehler auftritt, wenn das Objekt <code>match</code> vor dem Operator <code>null</code> ist, sondern das Ergebnis von <code>match?.group(0)</code> ist dann <code>null</code>. Der Operator <code>??</code> wird dann aktiv, wenn er Operand vor dem Operator null ist, dann ist das Gesamtergebnis der Operand nach dem Operator, also der Leerstring. | ||
* <code>groupNo = placeholder.codeUnitAt(1) - '0'.codeUnitAt(0);</code> | |||
** <code>placeholder.codeUnitAt(1)</code> liefert den Ganzzahlwert des 2.ten Zeichens (gezählt ab 0), von diesem wird der Ganzzahlwert der Ziffer '0' abgezogen. Da die Ganzzahlwerte der Ziffern "hintereinander" liegen, ergibt die Differenz die gewünschte Gruppennummer. | |||
** Eine verständliche aber inneffizentere Variante wäre: | |||
** | ** <code>groupNo = int.parse(placeholder[1]);</code> | ||
== Methode search() == | == Methode search() == | ||
Die Methode bereitet die Suche vor und startet sie. | Die Methode bereitet die Suche vor und startet sie. | ||
<pre>void search() { | <pre>/// Prepares the search and do it. | ||
void search() { | |||
String exitMessage; | String exitMessage; | ||
try { | try { | ||
final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern; | final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern; | ||
regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase); | regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase); | ||
final | final regExpList = <RegExp>[]; | ||
final paths = <String>[]; | final paths = <String>[]; | ||
for (var item in filePatterns) { | for (var item in filePatterns) { | ||
if (FileSystemEntity.isDirectorySync(item)) { | if (FileSystemEntity.isDirectorySync(item)) { | ||
paths.add(item); | paths.add(item); | ||
regExpList.add(null); | |||
} else { | } else { | ||
final directory = dirname(item); | |||
paths.add( | final filePattern = basename(item); | ||
paths.add(item.isEmpty ? '.' : directory); | |||
regExpList.add(filePattern.isEmpty ? null : RegExp(shellPatternToRegExp(filePattern))); | |||
} | } | ||
} | } | ||
try { | try { | ||
for (var ix = 0; ix < paths.length; ix++) { | for (var ix = 0; ix < paths.length; ix++) { | ||
searchFilePattern( | searchFilePattern(regExpList[ix], paths[ix], 0); | ||
} | } | ||
} on ExitException catch (exc) { | } on ExitException catch (exc) { | ||
Zeile 225: | Zeile 230: | ||
* <code>final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern;</code> | * <code>final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern;</code> | ||
** Bedingter Ausdruck: | ** Bedingter Ausdruck: | ||
** Wenn die Option <code>--word</code> gesetzt wurde, wird an das Suchmuster ein <code>\b</code vorangestellt und angehängt: dieses Metazeichen steht für nicht für einen String, sondern für eine Wortgrenze, also genau das was wir hier brauchen. | ** Wenn die Option <code>--word</code> gesetzt wurde, wird an das Suchmuster ein <code>\b</code> vorangestellt und angehängt: dieses Metazeichen steht für nicht für einen String, sondern für eine Wortgrenze, also genau das was wir hier brauchen. | ||
** Ohne die Word-Option wird das Muster <code>pattern</code> direkt verwendet. | ** Ohne die Word-Option wird das Muster <code>pattern</code> direkt verwendet. | ||
* <code>regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase);</code> | * <code>regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase);</code> | ||
Zeile 232: | Zeile 237: | ||
** <code>if (FileSystemEntity.isDirectorySync(item))</code> Test, ob die Angabe nur ein Verzeichnis ist. | ** <code>if (FileSystemEntity.isDirectorySync(item))</code> Test, ob die Angabe nur ein Verzeichnis ist. | ||
** Wenn ja, Eintrag in die Pfadliste und <code>null</code> in die Namensmusterliste (für "alle Dateien") | ** Wenn ja, Eintrag in die Pfadliste und <code>null</code> in die Namensmusterliste (für "alle Dateien") | ||
** Wenn nein, wird die Angabe in | ** Wenn nein, wird die Angabe in Verzeichnis <code>directory</code> und Dateinamensmuster <code>filePattern</code> zerlegt. | ||
** In die Pfadliste wird das aktuelle Verzeichnis <code>'.'</code> eingetragen, wenn <code>directory</code> leer ist, sonst das Verzeichnis. | |||
* <code>for (var ix = 0; ix < paths.length; ix++)</code> eine Schleife über alle Indizes der Pfadliste. | |||
* <code>searchFilePattern(regExprList[ix], paths[ix], 0);</code> Diese Methode realisiert die eigentliche Suche. | |||
* <code>for (var ix = 0; ix < paths.length; ix++)</code> eine Schleife über alle | |||
* <code>searchFilePattern(regExprList[ix], paths[ix] | |||
* <code>try { ... } on ExitException catch (exc)</code> Hier werden die "Schnellabbrüche" aufgefangen. Das passiert, wenn eine Bedingung der Optionen <code>--exit-lines</code> oder <code>--exit-files</code> erfüllt ist. | * <code>try { ... } on ExitException catch (exc)</code> Hier werden die "Schnellabbrüche" aufgefangen. Das passiert, wenn eine Bedingung der Optionen <code>--exit-lines</code> oder <code>--exit-files</code> erfüllt ist. | ||
* <code>try {...} on FormatException catch (exc) {</code> Die Ausnahme wird geworfen, wenn der reguläre Ausdruck in <code>regExp = RegExp(pattern2,...)</code> inkorrekt ist. Es wird dann eine Fehlermeldung mittels der Funktion <code>usage()</code> ausgegeben. | * <code>try {...} on FormatException catch (exc) {</code> Die Ausnahme wird geworfen, wenn der reguläre Ausdruck in <code>regExp = RegExp(pattern2,...)</code> inkorrekt ist. Es wird dann eine Fehlermeldung mittels der Funktion <code>usage()</code> ausgegeben. | ||
Zeile 276: | Zeile 279: | ||
lastShowedLine = lineNo; | lastShowedLine = lineNo; | ||
} | } | ||
} | continue; | ||
} | |||
hitLines++; | |||
totalHitLines++; | |||
if (searchOptions.list) { | |||
showLine(file); | |||
break; | |||
} | |||
aboveBound = lineNo + searchOptions.aboveContext; | |||
if (!searchOptions.count) { | |||
if (searchOptions.belowContext > 0) { | |||
for (var lineNo2 = lineNo - searchOptions.belowContext; | |||
lineNo2 < lineNo; | |||
lineNo2++) { | |||
if (lineNo2 > lastShowedLine) { | |||
line2 = formatLine(searchOptions.formatContext, file, | |||
lines[lineNo2 - 1], lineNo2, '<'); | |||
showLine(line2); | |||
} | } | ||
} | } | ||
} | } | ||
line2 = formatLine( | |||
searchOptions.format, file, line, lineNo, prefixMatch, | |||
match: match); | |||
showLine(line2); | |||
lastShowedLine = lineNo; | |||
} | |||
if (searchOptions.exitLines != null && | |||
totalHitLines >= searchOptions.exitLines) { | |||
throw ExitException('hit lines: $totalHitLines'); | |||
} | |||
if (searchOptions.breakLines != null && | |||
hitLines >= searchOptions.breakLines) { | |||
break; | |||
} | } | ||
} | } | ||
Zeile 322: | Zeile 325: | ||
} | } | ||
} | } | ||
</pre> | </pre> | ||
* <code>final lines = File(file).readAsLinesSync();</code> Die Klasse <code>File></code> instantiiert ein Objekt mit dem Dateinamen <code>file</code>, die Methode <code>readAsLinesSync()</code> liefert den Dateiinhalt als Liste von Zeilen. | * <code>final lines = File(file).readAsLinesSync();</code> Die Klasse <code>File></code> instantiiert ein Objekt mit dem Dateinamen <code>file</code>, die Methode <code>readAsLinesSync()</code> liefert den Dateiinhalt als Liste von Zeilen. | ||
Zeile 328: | Zeile 332: | ||
* <code>isHit = !searchOptions.invertMatch && match != null || searchOptions.invertMatch && match == null;</code> | * <code>isHit = !searchOptions.invertMatch && match != null || searchOptions.invertMatch && match == null;</code> | ||
** Ein Treffer liegt vor, wenn die Option <code>--invert-match</code> gesetzt ist und das Suchmuster nicht gefunden wurde oder wenn Option <code>--invert-match</code> '''nicht''' gesetzt ist und das Suchmuster gefunden wurde. | ** Ein Treffer liegt vor, wenn die Option <code>--invert-match</code> gesetzt ist und das Suchmuster nicht gefunden wurde oder wenn Option <code>--invert-match</code> '''nicht''' gesetzt ist und das Suchmuster gefunden wurde. | ||
* <code>if (!isHit)</code> Wenn kein Treffer vorliegt, Behandlung des "nachfolgenden Contexts" (Option <code>--above-lines</code>) | * <code>if (!isHit)</code> Wenn kein Treffer vorliegt, Behandlung des "nachfolgenden Contexts" (Option <code>--above-lines</code>), am Ende ein <code>continue;</code>, damit die Schleife mit dem nächsten Zeile weitermacht. | ||
* <code>if (searchOptions.list)</code> Wenn die Option <code>--list</code> gesetzt ist, wird der Dateiname ausgegeben <code>showLine(file);</code> und die Suche in dieser Datei beendet (<code>break</code>). | * <code>if (searchOptions.list)</code> Wenn die Option <code>--list</code> gesetzt ist, wird der Dateiname ausgegeben <code>showLine(file);</code> und die Suche in dieser Datei beendet (<code>break</code>). | ||
* <code>if (!searchOptions.count)</code> Wenn die Option <code>--count</code> nicht gesetzt ist. | * <code>if (!searchOptions.count)</code> Wenn die Option <code>--count</code> nicht gesetzt ist, wird die gefundene Trefferzeile und evt. Umgebungzeilen (Option <code>--below-lines</code>) ausgegeben. | ||
* <code>line2 = formatLine(searchOptions.format, file, line, lineNo, prefixMatch, match: match);</code> Der Treffer wird aufbereitet ... | * <code>line2 = formatLine(searchOptions.format, file, line, lineNo, prefixMatch, match: match);</code> Der Treffer wird aufbereitet ... | ||
* <code>showLine(line2);</code> ... und ausgegeben | * <code>showLine(line2);</code> ... und ausgegeben | ||
Zeile 348: | Zeile 352: | ||
<pre>/// Searches the files matching the [filePattern] in a [directory]. | <pre>/// Searches the files matching the [filePattern] in a [directory]. | ||
/// This method is recursive on subdirectories. | /// This method is recursive on subdirectories. | ||
/// [depth] is the nesting level of the recursive calls. | |||
void searchFilePattern(RegExp filePattern, String directory, int depth) { | void searchFilePattern(RegExp filePattern, String directory, int depth) { | ||
if (verboseLevel >= 2 && depth <= 1 || verboseLevel >= 3) { | if (verboseLevel >= 2 && depth <= 1 || verboseLevel >= 3) { | ||
Zeile 357: | Zeile 362: | ||
for (var file in Directory(directory).listSync()) { | for (var file in Directory(directory).listSync()) { | ||
final name = file.path; | final name = file.path; | ||
final node = | final node = basename(name); | ||
if (FileSystemEntity.isDirectorySync(name)) { | if (FileSystemEntity.isDirectorySync(name)) { | ||
if (fileOptions.excludedDirs != null && | if (fileOptions.excludedDirs != null && | ||
Zeile 368: | Zeile 373: | ||
subDirectories.add(name); | subDirectories.add(name); | ||
} | } | ||
} | continue; | ||
} | |||
if (filePattern != null && filePattern.firstMatch(node) == null) { | |||
ignoredFiles++; | |||
if (verboseLevel >= 4) { | |||
print('= ignoring not matching $name'); | |||
} | } | ||
if (fileOptions.excluded != null && | continue; | ||
} | |||
if (fileOptions.excluded != null && | |||
fileOptions.excluded.firstMatch(node) != null) { | |||
ignoredFiles++; | |||
if (verboseLevel >= 4) { | |||
print('= ignoring excluded $name'); | |||
} | } | ||
if (!fileOptions.processBinaries && isBinary(name)) { | continue; | ||
} | |||
if (!fileOptions.processBinaries && isBinary(name)) { | |||
if (verboseLevel >= 4) { | |||
print('= ignoring binary $name'); | |||
} | } | ||
final safeHits = totalHitLines; | countBinaries++; | ||
continue; | |||
} | |||
final safeHits = totalHitLines; | |||
searchFile(name); | |||
if (totalHitLines > safeHits) { | |||
totalHitFiles++; | |||
if (searchOptions.exitFiles != null && | |||
totalHitFiles >= searchOptions.exitFiles) { | |||
throw ExitException('hit files: $totalHitFiles'); | |||
} | } | ||
} | } | ||
Zeile 417: | Zeile 422: | ||
** Ein Objekt der Klasse <code>Directory</code> wird instantiiert, mit dem Verzeichnisnamen als Parameter. Dieses Objekt liefert mit der Methode <code>listSync()</code> eine Liste von <code>File</code>-Objekten ab, die mit der Schleife abgearbeitet wird. | ** Ein Objekt der Klasse <code>Directory</code> wird instantiiert, mit dem Verzeichnisnamen als Parameter. Dieses Objekt liefert mit der Methode <code>listSync()</code> eine Liste von <code>File</code>-Objekten ab, die mit der Schleife abgearbeitet wird. | ||
* <code>name = file.path;</code> Das Attribut <code>path</code> liefert den vollen Namen der Datei. | * <code>name = file.path;</code> Das Attribut <code>path</code> liefert den vollen Namen der Datei. | ||
* <code>node = path.basename(name);</code> Die Funktion <code>basename()</code> entfernt die Pfadangabe vom Dateinamen | * <code>node = path.basename(name);</code> Die Funktion <code>basename()</code> entfernt die Pfadangabe vom Dateinamen. | ||
* <code>if (FileSystemEntity.isDirectorySync(name))</code> Es wird geprüft, ob die Datei ein Verzeichnis ist: Wenn ja, wird | * <code>if (FileSystemEntity.isDirectorySync(name))</code> Es wird geprüft, ob die Datei ein Verzeichnis ist: Wenn ja, wird untersucht, ob ein Ausschluss des Verzeichnisses mittels der Option <code>--excluded-dirs</code> definiert ist. Wenn nicht, wird der volle Dateiname an die Liste der Unterverzeichnisse angehängt: <code>subDirectories.add(name);</code> und die Schleife mit <code>continue;</code> fortgesetzt, also mit der nächsten Zeile weitergemacht. | ||
* <code>if (filePattern != null && filePattern.firstMatch(node) == null)</code> | * <code>if (filePattern != null && filePattern.firstMatch(node) == null)</code> | ||
** Es wird geprüft, ob ein Dateimuster vorliegt (<code>filePattern != null</code>), aber kein Treffer vorliegt: Dann kommt die Datei nicht in Frage. | ** Es wird geprüft, ob ein Dateimuster vorliegt (<code>filePattern != null</code>), aber kein Treffer vorliegt: Dann kommt die Datei nicht in Frage. | ||
** <code>continue;</code> es geht mit dem nächsten Eintrag aus der <code>File</code>-Liste weiter. | ** <code>continue;</code> es geht mit dem nächsten Eintrag aus der <code>File</code>-Liste weiter. | ||
* <code>if (fileOptions.excluded != null && fileOptions.excluded.firstMatch(node) != null)</code> | * <code>if (fileOptions.excluded != null && fileOptions.excluded.firstMatch(node) != null)</code> | ||
** Wenn eine Dateiauschlussoption (code>--excluded</code> definiert ist, und das Suchmuster passt, kommt die Datei nicht in Frage, es geht mit dem nächsten Eintrag weiter (<code>continue</code>). | ** Wenn eine Dateiauschlussoption (<code>--excluded</code> definiert ist, und das Suchmuster passt, kommt die Datei nicht in Frage, es geht mit dem nächsten Eintrag weiter (<code>continue</code>). | ||
* <code>if (!fileOptions.processBinaries && isBinary(name))</code>Wenn die Option <code>--process-binary</code> nicht gesetzt ist und die Datei binär ist, geht es zum nächsten Listeneintrag (<code>continue</code>). | * <code>if (!fileOptions.processBinaries && isBinary(name))</code>Wenn die Option <code>--process-binary</code> nicht gesetzt ist und die Datei binär ist, geht es zum nächsten Listeneintrag (<code>continue</code>). Die Funktion <code>isBinary</code> stammt aus der [[Datei helper.dart]]. | ||
* <code>searchFile(name);</code> Hier findet die Suche in der Datei statt. | * <code>searchFile(name);</code> Hier findet die Suche in der Datei statt. | ||
* <code>if (totalHitLines > safeHits)</code> Wenn sich die Zahl der Trefferzeilen geändert hat, erhöht sich die Zahl der Trefferdateien. Wenn mit der Option <code>--exit-files</code> hier eine Grenze definiert wurde und diese erreicht ist, wird ein Schnellausstieg mit dem Werfen der Ausnahme <code>ExitException</code> getätigt. | * <code>if (totalHitLines > safeHits)</code> Wenn sich die Zahl der Trefferzeilen geändert hat, erhöht sich die Zahl der Trefferdateien. Wenn mit der Option <code>--exit-files</code> hier eine Grenze definiert wurde und diese erreicht ist, wird ein Schnellausstieg mit dem Werfen der Ausnahme <code>ExitException</code> getätigt. | ||
* <code>try { ... } on FileSystemException {</code> Beim Aufruf der Methode <code>listSync()</code> kann ein Problem auftreten, das das Werfen der Ausnahme <code>FileSystemException</code> auslöst. In diesem Fall wird die Statistik berichtigt und die Suche in diesem Verzeichnis beendet. | * <code>try { ... } on FileSystemException {</code> Beim Aufruf der Methode <code>listSync()</code> kann ein Problem auftreten, das das Werfen der Ausnahme <code>FileSystemException</code> auslöst. In diesem Fall wird die Statistik berichtigt und die Suche in diesem Verzeichnis beendet. | ||
* <code>if (fileOptions.recursive)</code> Wenn die Option <code>--recursive</code> gesetzt ist, wird die Liste der Unterverzeichnisse in einer Schleife <code>for (var subDir in subDirectories)</code> abgearbeitet. Das geschieht durch Aufruf der Methode <code>searchFilePattern()</code>, also einem '''rekursiven Aufruf''' von "sich selber". | * <code>if (fileOptions.recursive)</code> Wenn die Option <code>--recursive</code> gesetzt ist, wird die Liste der Unterverzeichnisse in einer Schleife <code>for (var subDir in subDirectories)</code> abgearbeitet. Das geschieht durch Aufruf der Methode <code>searchFilePattern()</code>, also einem '''rekursiven Aufruf''' von "sich selber". Die Verschachtelungstiefe <code>depth</code> erhöht sich dann um <code>1</code>. | ||
== Die Methode showLine() == | == Die Methode showLine() == | ||
Eine übersichtliche Methode, die entweder die übergebenen Zeile in der Klassenvariablen <code>lines</code> speichert (anhängt), was für Unittests benötigt wird, oder die Zeile mittels <code>print()</code> | Eine übersichtliche Methode, die entweder die übergebenen Zeile in der Klassenvariablen <code>lines</code> speichert (anhängt), was für Unittests benötigt wird, oder die Zeile mittels <code>print()</code> ausgibt. | ||
<pre>/// Prints or stores the output line, depending on the option [storeResult]. | <pre>/// Prints or stores the output line, depending on the option [storeResult]. | ||
void showLine(String line) { | void showLine(String line) { | ||
Zeile 441: | Zeile 446: | ||
} | } | ||
</pre> | </pre> | ||
== Die Methode addFlags() == | == Die Methode addFlags() == | ||
Die Methode fügt zum Objekt der Klasse <code>ArgParser</code> aus dem externen Paket <code>args</code> die Beschreibungen der Boolschen Optionen (genannt "Flags") hinzu. | Die Methode fügt zum Objekt der Klasse <code>ArgParser</code> aus dem externen Paket <code>args</code> die Beschreibungen der Boolschen Optionen (genannt "Flags") hinzu. | ||
<pre>static void addFlags(ArgParser parser) { | <pre>// Adds all boolean options to the argument [parser]. | ||
static void addFlags(ArgParser parser) { | |||
parser.addFlag('count', | parser.addFlag('count', | ||
abbr: 'c', | abbr: 'c', | ||
Zeile 453: | Zeile 460: | ||
... | ... | ||
} | } | ||
</pre> | |||
* <code>parser.addFlag('count',</code> Hinzufügen eines Optionsflags namens <code>count</code>, das bedeutet, die Option wird mit <code>--count</code> in der Kommandozeile aufgerufen. | * <code>parser.addFlag('count',</code> Hinzufügen eines Optionsflags namens <code>count</code>, das bedeutet, die Option wird mit <code>--count</code> in der Kommandozeile aufgerufen. | ||
* <code>abbr: 'c',</code> Es gibt eine abkürzende Notierung, nämlich <code>-c</code>. | * <code>abbr: 'c',</code> Es gibt eine abkürzende Notierung, nämlich <code>-c</code>. | ||
* <code>help: 'Show only the count of lines with hits (per file)',</code> Eine Beschreibung der Option. Wird das Attribut <code>usage</code> der Klasse <code>ArgParser</code> abgefragt, erscheint dieser Hilfetext darin. | * <code>help: 'Show only the count of lines with hits (per file)',</code> Eine Beschreibung der Option. Wird das Attribut <code>usage</code> der Klasse <code>ArgParser</code> abgefragt, erscheint dieser Hilfetext darin. | ||
* <code>negatable: false);</code> Normalerweise kann ein Flag auch in invertierter Form aufgerufen werden, <code>no-count</ | * <code>negatable: false);</code> Normalerweise kann ein Flag auch in invertierter Form aufgerufen werden, <code>no-count</code> würde den Wert negieren. Das wird mit diesem Parameter <code>negatable</code> ausgeschaltet. | ||
* Der Rest der Methode funktioniert nach dem gleichen Schema. | * Der Rest der Methode funktioniert nach dem gleichen Schema für alle anderen Boolschen Optionen. | ||
== Die Methode addOption() == | == Die Methode addOption() == | ||
Die Methode fügt zum Objekt der Klasse <code>ArgParser</code> aus dem externen Paket <code>args</code> die Beschreibungen von Optionen hinzu. | Die Methode fügt zum Objekt der Klasse <code>ArgParser</code> aus dem externen Paket <code>args</code> die Beschreibungen von Optionen hinzu. | ||
<pre>static void addOptions(ArgParser parser) { | <pre>// Adds all not boolean options to the argument [parser]. | ||
static void addOptions(ArgParser parser) { | |||
parser.addOption('excluded', | parser.addOption('excluded', | ||
abbr: 'x', | abbr: 'x', | ||
Zeile 473: | Zeile 482: | ||
} | } | ||
</pre> | </pre> | ||
* <code>parser.addOption('excluded',</code> definiert eine Option namens <code>excluded</code>. Die Option kann in der Kommandzeile mit <code>--excluded=<string></code> aufgerufen werden, oder mit <code>--excluded <string></code>. | * <code>parser.addOption('excluded',</code> definiert eine Option namens <code>excluded</code>. Die Option kann in der Kommandzeile mit <code>--excluded=<string></code> (Wert mit '=' abgetrennt) aufgerufen werden, oder mit <code>--excluded <string></code> (Wert mit Leerzeichen abgetrennt). | ||
* <code>abbr: 'x',</code> Definiert die Abkürzung, aufzurufen mit <code>-x<string></code> bzw. <code>-x <string></code>. | * <code>abbr: 'x',</code> Definiert die Abkürzung, aufzurufen mit <code>-x<string></code> (ohne Trennzeichen) bzw. <code>-x <string></code> (mit Leerzeichen dazwischen). | ||
* <code>help: 'A regular expression for files to skip, e.g. ".*\.(bak|sic)"');</code> definiert die Beschreibung, die für den Hilfetext verwendet wird. | * <code>help: 'A regular expression for files to skip, e.g. ".*\.(bak|sic)"');</code> definiert die Beschreibung, die für den Hilfetext verwendet wird. | ||
* Die übrigen Optionen werden nach dem gleichen Schema definiert. | * Die übrigen Optionen werden nach dem gleichen Schema definiert. | ||
== Die Methode == | == Die Methode execute() == | ||
Diese statische Methode kann in <code>main()</code> aufgerufen werden, sie übernimmt die | Diese statische Methode kann in <code>main()</code> aufgerufen werden, sie übernimmt die | ||
Programmargumente mit dem Parameter <code>arguments</code>. | Programmargumente mit dem Parameter <code>arguments</code>. | ||
Zeile 487: | Zeile 496: | ||
Das ist eine sinnvolle Designentscheidung. | Das ist eine sinnvolle Designentscheidung. | ||
<pre>static SearchEngine execute(List<String> arguments) { | <pre>/// Executes the total search defined by the program [arguments]. | ||
static SearchEngine execute(List<String> arguments) { | |||
SearchEngine engine; | SearchEngine engine; | ||
final parser = ArgParser(); | final parser = ArgParser(); | ||
addFlags(parser); | addFlags(parser); | ||
addOptions(parser); | addOptions(parser); | ||
final results = parser.parse(arguments); | try { | ||
final results = parser.parse(arguments); | |||
final intArgs = [ | |||
'above-context', | |||
'context', | |||
'below-context', | |||
'break-lines', | |||
'exit-lines', | |||
'exit-files', | |||
'verbose-level' | |||
]; | |||
if (results['help']) { | |||
usage(null, parser: parser); | |||
} else if (results.rest.isEmpty) { | |||
usage('too few arguments'); | |||
} else if (testIntArguments(results, intArgs, usage) && | |||
testRegExpArguments(results, ['excluded', 'excluded-dirs'], usage)) { | |||
if (results.rest.isEmpty) { | |||
usage('too few arguments', parser: parser); | |||
} else { | |||
engine = SearchEngine(results.rest[0], | |||
results.rest.length == 1 ? ['.'] : results.rest.sublist(1), | |||
searchOptions: SearchOptions.fromArgument(results), | |||
verboseLevel: intValue(results['verbose-level']), | |||
fileOptions: FileOptions.fromArgument(results)); | |||
engine.search(); | |||
} | |||
} | } | ||
} on FormatException catch (exc) { | |||
usage(exc.toString()); | |||
} | } | ||
return engine; | return engine; | ||
Zeile 533: | Zeile 539: | ||
* <code>addFlags(parser);</code> und <code>addOptions(parser);</code> fügen die Optionen dazu. | * <code>addFlags(parser);</code> und <code>addOptions(parser);</code> fügen die Optionen dazu. | ||
* <code>results = parser.parse(arguments);</code> Die Verarbeitung der Programmargumente wird erledigt, Ergebnis ist ein Objekt vom Typ <code>ArgResults</code>. | * <code>results = parser.parse(arguments);</code> Die Verarbeitung der Programmargumente wird erledigt, Ergebnis ist ein Objekt vom Typ <code>ArgResults</code>. | ||
* <code>if (results['help'])</code> Wurde die Option <code>--help</code> | * <code>if (results['help'])</code> Wurde die Option <code>--help</code> angegeben? Wenn ja wird die Beschreibung mittels <code>usage(null, parser: parser);</code> ausgegeben, ohne Fehlermeldung (erster Parameter ist <code>null</code>). Wir sehen hier, dass auf die Optionen von <code>results</code> mittels der eckigen Klammern zugegriffen werden kann, wie bei einer Map. | ||
* <code>if (testIntArguments(...)</code> Test, ob die Optionen mit Ganzzahlen korrekt belegt wurden. | * <code>if (results.rest.isEmpty)</code> Das Attribut <code>rest</code> der Klasse <code>ArgResults</code> enthält alle Programmargumente, die keine Optionen sind, die also nicht mit '-' anfangen. Diese Liste wird getestet, ob sie leer ist. Wenn ja, liegt der Fehler "zu wenig Argumente" vor, der mit der Funktion <code>usage()</code> ausgegeben wird. | ||
Siehe [[Datei helper.dart]]. | * <code>if (testIntArguments(...)</code> Test, ob die Optionen mit Ganzzahlen korrekt belegt wurden. Siehe [[Datei helper.dart]]. | ||
* <code>&& testRegExpArguments(...)</code> Test, ob die regulären Ausdrücke korrekt sind. Siehe [[Datei helper.dart]]. | * <code>&& testRegExpArguments(...)</code> Test, ob die regulären Ausdrücke korrekt sind. Siehe [[Datei helper.dart]]. | ||
* <code>engine = SearchEngine(...)</code> Die Suchverarbeitung wird initialisiert... | * <code>engine = SearchEngine(...)</code> Die Suchverarbeitung wird initialisiert... | ||
* <code>engine.search()</code> ... und durchgeführt. | * <code>engine.search()</code> ... und durchgeführt. | ||
* <code>return engine;/code> Das Ergebnis ist das Objekt mit den Suchergebnissen. | * <code>return engine;</code> Das Ergebnis ist das Objekt mit den Suchergebnissen. | ||
= Die Klasse SearchOptions = | = Die Klasse SearchOptions = |
Aktuelle Version vom 5. Januar 2021, 00:47 Uhr
Links[Bearbeiten]
Die Funktion usage()[Bearbeiten]
Die Funktion usage() gibt eine Beschreibung der Benutzung des Programms aus und evt. eine Fehlermeldung.
/// Shows a message how to use the program. /// [message]: null or an error message /// [parser]: delivers the description of the program's options void usage(String message, {ArgParser parser}) { print('''Usage: dgrep [<options>] <pattern> [<file1> [<file2> ...]] Searches <pattern> in <file1> <file2>... <fileN>: a directory or a shell file pattern like '*.txt' or both like /home/*.txt <option>:'''); if (parser == null) { print(' Use -h or --help for more info'); } else { print(parser.usage); } if (message != null) { print('+++ ' + message); } }
///
markiert einen Kommentar, der automatisch für die Dokumentation ausgewertet wird.print(parser.usage);
Das Attributusage
der KlasseArgParser
gibt eine automatisch erstellte Beschreibung der Optionen aus.- Es wird eine mehrzeiliger Stringkonstante benutzt, die mit drei Apostrophen
"'Usage:...<option>:'"
eingeschlossen ist. Hinweis: drei Apostrophe sind im Wiki nicht darstellbar, daher die "Simulation" mit Gänsefüßchen und Apostroph.
Klasse ExitException[Bearbeiten]
Diese Klasse wird als Ausnahme benutzt, um aus einer verschachtelten Situation unkompliziert ans Ende des Programms zu gelangen, wenn bestimmte Bedingungen erfüllt sind.
/// Used for jump out of nested calls. class ExitException { final String reason; ExitException(this.reason); }
Die Klasse FileOptions[Bearbeiten]
Diese Klasse speichert die Optionen, die sich auf die Dateiauswahl beziehen.
/// Stores file selection options. class FileOptions { bool recursive; RegExp excluded; RegExp excludedDirs; bool processBinaries; FileOptions( {this.recursive = false, this.excluded, this.excludedDirs, this.processBinaries = false}); FileOptions.fromArgument(ArgResults results) { recursive = results['recursive']; excluded = results['excluded'] == null ? null : RegExp(results['excluded']); excludedDirs = results['excluded-dirs'] == null ? null : RegExp(results['excluded-dirs']); processBinaries = results['process-binaries']; } }
- Es existieren zwei Konstruktoren:
FileOptions()
wird nur in Tests benutzt.FileOptions.fromArgument()
holt die Optionen aus einem Objekt der KlasseArgResult
aus dem externen Paketargs
.
recursive = results['recursive'];
Das boolsche Attributrecursive
wird direkt aus dem Objekt der KlasseArgResult
übernommen. Wir lernen: Zugriff auf die Optionen erfolgt mittels Namenrecursive
mit dem eckige-Klammer-Operator wie bei Maps.
Die Klasse SearchEngine[Bearbeiten]
Die Klasse implementiert ("realisiert") die Suche.
Die Attribute[Bearbeiten]
Zuerst werden die Attribute definiert:
/// Searches a regular expression in files. class SearchEngine { static bool storeResult = false; final List<String> filePatterns; final String pattern; final SearchOptions searchOptions; final FileOptions fileOptions; int handledFiles = 0; int handledDirs = 0; int totalHitLines = 0; int totalHitFiles = 0; int ignoredFiles = 0; int ignoredDirs = 0; int countBinaries = 0; int verboseLevel = 4; RegExp regExp; String rc; final lines = <String>[]; static final formatPlaceholders = RegExp(r'%[#fpnFehcl1-9]%');
- Das Attribut
formatPlaceholders
ist alsstatic
definiert, damit findet die Initialiserung nur einmal statt, nicht bei jedem Objekt. Das ist möglich, weil sich der Ausdruck zur Beschreibung eines Platzhalters im Formatstring nie was ändert.
Der Konstruktor[Bearbeiten]
SearchEngine(this.pattern, this.filePatterns, {this.fileOptions, this.searchOptions, this.verboseLevel = 0});
- Es werden das Suchmuster
pattern
, eine Liste von DateimusternfilePatterns
, Dateioptionen, Suchoptionen und ein Level für MeldungenverboseLevel
übergeben und gespeichert.
Die Methode formatLine()[Bearbeiten]
Diese Methode stellt einen String zur Ausgabe zusammen, gesteuert durch ein Format, das per Option definiert wird.
Die notwendigen Daten werden per Parameter übergeben, auch das Format, da es zwei Formate gibt: das für Trefferzeilen und das für Zeilen "der Umgebung" (siehe Option --above-lines
oder below-lines
).
/// Creates an output line depending on a given [format]. String formatLine( String format, String file, String line, int lineNo, String prefix, {RegExpMatch match, int hits}) { String line2; if (format == null) { line2 = '$prefix$file-$lineNo: $line'; } else { line2 = searchOptions.format; for (var match2 in formatPlaceholders.allMatches(format)) { final placeholder = match2.group(0); switch (placeholder[1]) { case '#': line2 = line2.replaceAll(placeholder, lineNo.toString()); break; case 'f': line2 = line2.replaceAll(placeholder, file); break; case 'p': line2 = line2.replaceAll(placeholder, dirname(file)); break; case 'n': line2 = line2.replaceAll(placeholder, basename(file)); break; case 'F': line2 = line2.replaceAll( placeholder, basenameWithoutExtension(file)); break; case 'e': line2 = line2.replaceAll(placeholder, extension(file)); break; case 'h': line2 = line2.replaceAll(placeholder, match?.group(0) ?? ''); break; case 'c': line2 = line2.replaceAll(placeholder, hits?.toString() ?? ''); break; case 'l': line2 = line2.replaceAll(placeholder, line); break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': final groupNo = placeholder.codeUnitAt(1) - '0'.codeUnitAt(0); line2 = line2.replaceAll(placeholder, match?.group(groupNo) ?? ''); break; default: break; } } } return line2; }
if (format == null)
Wenn das Formatnull
ist, liegt keine Definition per Option vor, es wird ein Standardformat benutzt, mit Dateiname, Zeilennummer und Zeile.- Ist ein Format definiert:
line2 = searchOptions.format;
Das Format wird kopiert in die Variableline2
kopiert, da das Orginal unverändert bleiben muss.for (var match2 in formatPlaceholders.allMatches(format))
formatPlaceholders
ist ein regulärer Ausdruck, der alle Platzhalter im Format beschreibt:RegExp(r'%[#fpnFehcl1-9]%')
Zwei Prozentzeichen, zwischen denen eines der aufgeführten Zeichen#
...l
oder eine Ziffer1-9
liegt.- Die Klasse
RegExp
liefert mit der MethodeallMatches()
nacheinander alle Treffer des regulären Ausdrucks in dem als Parameter übergebenen String (hierformat
) liefert. Über diese Treffer wird die Schleife gebildet.
final placeholder = match2.group(0);
Der Treffer (beispielsweise%f%
) wird in der Variablenplaceholder
gemerkt.switch(placeholder[1])
Es wird das zweite Zeichen (gezählt wird ab 0) als Fallunterscheidung genommen:case '#':
Ist dieses 2.te Zeichen ein#
...line2 = line2.replaceAll(placeholder, lineNo.toString());
Es werden alle diese Platzhalter (%#%
) durch die Zeilenummer, gewandelt in einen String, ersetzt.
- Nach diesem Schema werden die auch die anderen Platzhalter ersetzt.
match?.group(0) ?? "
Der Operator?.
sorgt dafür, dass kein Fehler auftritt, wenn das Objektmatch
vor dem Operatornull
ist, sondern das Ergebnis vonmatch?.group(0)
ist dannnull
. Der Operator??
wird dann aktiv, wenn er Operand vor dem Operator null ist, dann ist das Gesamtergebnis der Operand nach dem Operator, also der Leerstring.
groupNo = placeholder.codeUnitAt(1) - '0'.codeUnitAt(0);
placeholder.codeUnitAt(1)
liefert den Ganzzahlwert des 2.ten Zeichens (gezählt ab 0), von diesem wird der Ganzzahlwert der Ziffer '0' abgezogen. Da die Ganzzahlwerte der Ziffern "hintereinander" liegen, ergibt die Differenz die gewünschte Gruppennummer.- Eine verständliche aber inneffizentere Variante wäre:
groupNo = int.parse(placeholder[1]);
Methode search()[Bearbeiten]
Die Methode bereitet die Suche vor und startet sie.
/// Prepares the search and do it. void search() { String exitMessage; try { final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern; regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase); final regExpList = <RegExp>[]; final paths = <String>[]; for (var item in filePatterns) { if (FileSystemEntity.isDirectorySync(item)) { paths.add(item); regExpList.add(null); } else { final directory = dirname(item); final filePattern = basename(item); paths.add(item.isEmpty ? '.' : directory); regExpList.add(filePattern.isEmpty ? null : RegExp(shellPatternToRegExp(filePattern))); } } try { for (var ix = 0; ix < paths.length; ix++) { searchFilePattern(regExpList[ix], paths[ix], 0); } } on ExitException catch (exc) { exitMessage = '= search stopped: ' + exc.reason; } if (verboseLevel >= 1) { var hits = searchOptions.count || searchOptions.list ? '' : ' matching lines: $totalHitLines'; print( '= processed directories: $handledDirs processed files: $handledFiles$hits'); print( '= ignored directories: $ignoredDirs ignored files: $ignoredFiles binary files: $countBinaries'); if (exitMessage != null) { print(exitMessage); } } } on FormatException catch (exc) { usage('error in regular expression "$pattern": $exc'); } }
final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern;
- Bedingter Ausdruck:
- Wenn die Option
--word
gesetzt wurde, wird an das Suchmuster ein\b
vorangestellt und angehängt: dieses Metazeichen steht für nicht für einen String, sondern für eine Wortgrenze, also genau das was wir hier brauchen. - Ohne die Word-Option wird das Muster
pattern
direkt verwendet.
regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase);
- Die Option
--ignore-case
wird hier berücksichtigt. Da case-sensitiv die logische Umkehrung von ignore-case ist, wird der logische Operator!
eingesetzt, der austrue
false
macht und umgekehrt.
- Die Option
for (var item in filePatterns)
Die Dateinamensmuster können Pfade enthalten oder nicht, diese Info wird in dieser Schleife ermittelt, in Form von zwei Listen mit Pfadenpaths
und mit DateimusterregExprList
if (FileSystemEntity.isDirectorySync(item))
Test, ob die Angabe nur ein Verzeichnis ist.- Wenn ja, Eintrag in die Pfadliste und
null
in die Namensmusterliste (für "alle Dateien") - Wenn nein, wird die Angabe in Verzeichnis
directory
und DateinamensmusterfilePattern
zerlegt. - In die Pfadliste wird das aktuelle Verzeichnis
'.'
eingetragen, wenndirectory
leer ist, sonst das Verzeichnis.
for (var ix = 0; ix < paths.length; ix++)
eine Schleife über alle Indizes der Pfadliste.searchFilePattern(regExprList[ix], paths[ix], 0);
Diese Methode realisiert die eigentliche Suche.try { ... } on ExitException catch (exc)
Hier werden die "Schnellabbrüche" aufgefangen. Das passiert, wenn eine Bedingung der Optionen--exit-lines
oder--exit-files
erfüllt ist.try {...} on FormatException catch (exc) {
Die Ausnahme wird geworfen, wenn der reguläre Ausdruck inregExp = RegExp(pattern2,...)
inkorrekt ist. Es wird dann eine Fehlermeldung mittels der Funktionusage()
ausgegeben.
Die Methode searchFile()[Bearbeiten]
Die Methode durchsucht eine Datei nach dem spezifizierten Suchstring. Sie ist relativ umfangreich, was an den vielen Suchoptionen liegt.
/// Read the [file]'s content and search for the search pattern respecting /// the search options. /// Uses [showLine()] to show the matching lines. void searchFile(String file) { if (verboseLevel >= 3) { print('= processing $file ...'); } try { final lines = File(file).readAsLinesSync(); handledFiles++; var hitLines = 0; var lineNo = 0; var lastShowedLine = 0; var aboveBound = 0; var line2; final prefixMatch = searchOptions.belowContext > 0 || searchOptions.aboveContext > 0 ? '=' : ''; for (var line in lines) { lineNo++; final match = regExp.firstMatch(line); final isHit = !searchOptions.invertMatch && match != null || searchOptions.invertMatch && match == null; if (!isHit) { if (lineNo <= aboveBound) { line2 = formatLine( searchOptions.formatContext, file, line, lineNo, '>'); showLine(line2); lastShowedLine = lineNo; } continue; } hitLines++; totalHitLines++; if (searchOptions.list) { showLine(file); break; } aboveBound = lineNo + searchOptions.aboveContext; if (!searchOptions.count) { if (searchOptions.belowContext > 0) { for (var lineNo2 = lineNo - searchOptions.belowContext; lineNo2 < lineNo; lineNo2++) { if (lineNo2 > lastShowedLine) { line2 = formatLine(searchOptions.formatContext, file, lines[lineNo2 - 1], lineNo2, '<'); showLine(line2); } } } line2 = formatLine( searchOptions.format, file, line, lineNo, prefixMatch, match: match); showLine(line2); lastShowedLine = lineNo; } if (searchOptions.exitLines != null && totalHitLines >= searchOptions.exitLines) { throw ExitException('hit lines: $totalHitLines'); } if (searchOptions.breakLines != null && hitLines >= searchOptions.breakLines) { break; } } if (searchOptions.count) { showLine(searchOptions.format == null ? '$hitLines $file' : formatLine(searchOptions.format, file, '', 0, '', hits: hitLines)); } } on FileSystemException { ignoredFiles++; } }
final lines = File(file).readAsLinesSync();
Die KlasseFile>
instantiiert ein Objekt mit dem Dateinamenfile
, die MethodereadAsLinesSync()
liefert den Dateiinhalt als Liste von Zeilen.for (var line in lines)
iteriert über alle Zeilenfinal match = regExp.firstMatch(line);
Test, ob die Zeileline
das Suchmuster enthält.isHit = !searchOptions.invertMatch && match != null || searchOptions.invertMatch && match == null;
- Ein Treffer liegt vor, wenn die Option
--invert-match
gesetzt ist und das Suchmuster nicht gefunden wurde oder wenn Option--invert-match
nicht gesetzt ist und das Suchmuster gefunden wurde.
- Ein Treffer liegt vor, wenn die Option
if (!isHit)
Wenn kein Treffer vorliegt, Behandlung des "nachfolgenden Contexts" (Option--above-lines
), am Ende eincontinue;
, damit die Schleife mit dem nächsten Zeile weitermacht.if (searchOptions.list)
Wenn die Option--list
gesetzt ist, wird der Dateiname ausgegebenshowLine(file);
und die Suche in dieser Datei beendet (break
).if (!searchOptions.count)
Wenn die Option--count
nicht gesetzt ist, wird die gefundene Trefferzeile und evt. Umgebungzeilen (Option--below-lines
) ausgegeben.line2 = formatLine(searchOptions.format, file, line, lineNo, prefixMatch, match: match);
Der Treffer wird aufbereitet ...showLine(line2);
... und ausgegebenif (searchOptions.exitLines != null && totalHitLines >= searchOptions.exitLines)
Wenn die Option--exit-lines
gesetzt ist und die Zahl der Trefferzeilen erreicht ist, Schnellabbruch mit der AusnahmeExitException
.if (searchOptions.breakLines != null && hitLines >= searchOptions.breakLines)
Wenn die Option--break-lines
gesetzt ist und die Zahl der Trefferzeilen in der Datei erreicht ist, wird die Suchschleife beendet (break
).if (searchOptions.count)
Wenn die Option--count
gesetzt ist, wird die Anzahl der Treffer in dieser Datei ausgegeben (nach der Suchschleife, wenn die Treffer gezählt sind).try { ... } on FileSystemException {
Beim Lesen der Datei kann ein Problem auftreten, beispielsweise ein Rechteproblem, es wird dann die AusnahmeFileSystemException
geworfen. Wir zählen das Problem, fertig.
Die Methode searchFilePattern()[Bearbeiten]
Die Methode sucht die Dateien gemäß den Dateioptionen aus dem Dateibaum.
Die Methode ist rekursiv, das heißt sie ruft sich selber auf: Sie durchsucht ein Verzeichnis,
das mit dem Parameter directory
spezifiziert ist, auf passende Dateien und ermittelt nebenbei alle Unterverzeichnisse.
Danach ist eine Suche in den Unterverzeichnissen notwendig, und genau das kann die Methode, sie muss
sich nur mit einem anderen Parameter directory
aufrufen.
/// Searches the files matching the [filePattern] in a [directory]. /// This method is recursive on subdirectories. /// [depth] is the nesting level of the recursive calls. void searchFilePattern(RegExp filePattern, String directory, int depth) { if (verboseLevel >= 2 && depth <= 1 || verboseLevel >= 3) { print('= processing $directory ...'); } final subDirectories = <String>[]; try { handledDirs++; for (var file in Directory(directory).listSync()) { final name = file.path; final node = basename(name); if (FileSystemEntity.isDirectorySync(name)) { if (fileOptions.excludedDirs != null && fileOptions.excludedDirs.firstMatch(node) != null) { if (verboseLevel >= 4) { print('= ignoring not matching directory $name'); } ignoredDirs++; } else { subDirectories.add(name); } continue; } if (filePattern != null && filePattern.firstMatch(node) == null) { ignoredFiles++; if (verboseLevel >= 4) { print('= ignoring not matching $name'); } continue; } if (fileOptions.excluded != null && fileOptions.excluded.firstMatch(node) != null) { ignoredFiles++; if (verboseLevel >= 4) { print('= ignoring excluded $name'); } continue; } if (!fileOptions.processBinaries && isBinary(name)) { if (verboseLevel >= 4) { print('= ignoring binary $name'); } countBinaries++; continue; } final safeHits = totalHitLines; searchFile(name); if (totalHitLines > safeHits) { totalHitFiles++; if (searchOptions.exitFiles != null && totalHitFiles >= searchOptions.exitFiles) { throw ExitException('hit files: $totalHitFiles'); } } } } on FileSystemException { handledDirs--; ignoredDirs++; } if (fileOptions.recursive) { for (var subDir in subDirectories) { searchFilePattern(filePattern, subDir, depth + 1); } } }
final subDirectories = <String>[];
Eine leere Liste für die Unterverzeichnisse wird angelegt.for (var file in Directory(directory).listSync())
- Ein Objekt der Klasse
Directory
wird instantiiert, mit dem Verzeichnisnamen als Parameter. Dieses Objekt liefert mit der MethodelistSync()
eine Liste vonFile
-Objekten ab, die mit der Schleife abgearbeitet wird.
- Ein Objekt der Klasse
name = file.path;
Das Attributpath
liefert den vollen Namen der Datei.node = path.basename(name);
Die Funktionbasename()
entfernt die Pfadangabe vom Dateinamen.if (FileSystemEntity.isDirectorySync(name))
Es wird geprüft, ob die Datei ein Verzeichnis ist: Wenn ja, wird untersucht, ob ein Ausschluss des Verzeichnisses mittels der Option--excluded-dirs
definiert ist. Wenn nicht, wird der volle Dateiname an die Liste der Unterverzeichnisse angehängt:subDirectories.add(name);
und die Schleife mitcontinue;
fortgesetzt, also mit der nächsten Zeile weitergemacht.if (filePattern != null && filePattern.firstMatch(node) == null)
- Es wird geprüft, ob ein Dateimuster vorliegt (
filePattern != null
), aber kein Treffer vorliegt: Dann kommt die Datei nicht in Frage. continue;
es geht mit dem nächsten Eintrag aus derFile
-Liste weiter.
- Es wird geprüft, ob ein Dateimuster vorliegt (
if (fileOptions.excluded != null && fileOptions.excluded.firstMatch(node) != null)
- Wenn eine Dateiauschlussoption (
--excluded
definiert ist, und das Suchmuster passt, kommt die Datei nicht in Frage, es geht mit dem nächsten Eintrag weiter (continue
).
- Wenn eine Dateiauschlussoption (
if (!fileOptions.processBinaries && isBinary(name))
Wenn die Option--process-binary
nicht gesetzt ist und die Datei binär ist, geht es zum nächsten Listeneintrag (continue
). Die FunktionisBinary
stammt aus der Datei helper.dart.searchFile(name);
Hier findet die Suche in der Datei statt.if (totalHitLines > safeHits)
Wenn sich die Zahl der Trefferzeilen geändert hat, erhöht sich die Zahl der Trefferdateien. Wenn mit der Option--exit-files
hier eine Grenze definiert wurde und diese erreicht ist, wird ein Schnellausstieg mit dem Werfen der AusnahmeExitException
getätigt.try { ... } on FileSystemException {
Beim Aufruf der MethodelistSync()
kann ein Problem auftreten, das das Werfen der AusnahmeFileSystemException
auslöst. In diesem Fall wird die Statistik berichtigt und die Suche in diesem Verzeichnis beendet.if (fileOptions.recursive)
Wenn die Option--recursive
gesetzt ist, wird die Liste der Unterverzeichnisse in einer Schleifefor (var subDir in subDirectories)
abgearbeitet. Das geschieht durch Aufruf der MethodesearchFilePattern()
, also einem rekursiven Aufruf von "sich selber". Die Verschachtelungstiefedepth
erhöht sich dann um1
.
Die Methode showLine()[Bearbeiten]
Eine übersichtliche Methode, die entweder die übergebenen Zeile in der Klassenvariablen lines
speichert (anhängt), was für Unittests benötigt wird, oder die Zeile mittels print()
ausgibt.
/// Prints or stores the output line, depending on the option [storeResult]. void showLine(String line) { if (storeResult) { lines.add(line); } else { print(line); } }
Die Methode addFlags()[Bearbeiten]
Die Methode fügt zum Objekt der Klasse ArgParser
aus dem externen Paket args
die Beschreibungen der Boolschen Optionen (genannt "Flags") hinzu.
// Adds all boolean options to the argument [parser]. static void addFlags(ArgParser parser) { parser.addFlag('count', abbr: 'c', help: 'Show only the count of lines with hits (per file)', negatable: false); parser.addFlag('ignore-case', abbr: 'i', help: 'The search is case insensitive', negatable: false); ... }
parser.addFlag('count',
Hinzufügen eines Optionsflags namenscount
, das bedeutet, die Option wird mit--count
in der Kommandozeile aufgerufen.abbr: 'c',
Es gibt eine abkürzende Notierung, nämlich-c
.help: 'Show only the count of lines with hits (per file)',
Eine Beschreibung der Option. Wird das Attributusage
der KlasseArgParser
abgefragt, erscheint dieser Hilfetext darin.negatable: false);
Normalerweise kann ein Flag auch in invertierter Form aufgerufen werden,no-count
würde den Wert negieren. Das wird mit diesem Parameternegatable
ausgeschaltet.- Der Rest der Methode funktioniert nach dem gleichen Schema für alle anderen Boolschen Optionen.
Die Methode addOption()[Bearbeiten]
Die Methode fügt zum Objekt der Klasse ArgParser
aus dem externen Paket args
die Beschreibungen von Optionen hinzu.
// Adds all not boolean options to the argument [parser]. static void addOptions(ArgParser parser) { parser.addOption('excluded', abbr: 'x', help: 'A regular expression for files to skip, e.g. ".*\.(bak|sic)"'); parser.addOption('excluded-dirs', abbr: 'X', help: 'A regular expression for directory to skip, e.g. "test|\.git|\.config'); ... }
parser.addOption('excluded',
definiert eine Option namensexcluded
. Die Option kann in der Kommandzeile mit--excluded=<string>
(Wert mit '=' abgetrennt) aufgerufen werden, oder mit--excluded <string>
(Wert mit Leerzeichen abgetrennt).abbr: 'x',
Definiert die Abkürzung, aufzurufen mit-x<string>
(ohne Trennzeichen) bzw.-x <string>
(mit Leerzeichen dazwischen).help: 'A regular expression for files to skip, e.g. ".*\.(bak|sic)"');
definiert die Beschreibung, die für den Hilfetext verwendet wird.- Die übrigen Optionen werden nach dem gleichen Schema definiert.
Die Methode execute()[Bearbeiten]
Diese statische Methode kann in main()
aufgerufen werden, sie übernimmt die
Programmargumente mit dem Parameter arguments
.
Als Ergebnis wird ein Objekt der Klasse SearchEngine
zurückgegeben, das wird ausgiebig bei den Unittests genutzt, die die Attribute der Klasse dann untersuchen.
Durch diese Konstruktion können die Tests mit größtmöglicher Nähe zum "normalen" Ablauf ablaufen. Das ist eine sinnvolle Designentscheidung.
/// Executes the total search defined by the program [arguments]. static SearchEngine execute(List<String> arguments) { SearchEngine engine; final parser = ArgParser(); addFlags(parser); addOptions(parser); try { final results = parser.parse(arguments); final intArgs = [ 'above-context', 'context', 'below-context', 'break-lines', 'exit-lines', 'exit-files', 'verbose-level' ]; if (results['help']) { usage(null, parser: parser); } else if (results.rest.isEmpty) { usage('too few arguments'); } else if (testIntArguments(results, intArgs, usage) && testRegExpArguments(results, ['excluded', 'excluded-dirs'], usage)) { if (results.rest.isEmpty) { usage('too few arguments', parser: parser); } else { engine = SearchEngine(results.rest[0], results.rest.length == 1 ? ['.'] : results.rest.sublist(1), searchOptions: SearchOptions.fromArgument(results), verboseLevel: intValue(results['verbose-level']), fileOptions: FileOptions.fromArgument(results)); engine.search(); } } } on FormatException catch (exc) { usage(exc.toString()); } return engine; }
final parser = ArgParser();
Initialisierung der Verarbeitung von Programmargumenten.addFlags(parser);
undaddOptions(parser);
fügen die Optionen dazu.results = parser.parse(arguments);
Die Verarbeitung der Programmargumente wird erledigt, Ergebnis ist ein Objekt vom TypArgResults
.if (results['help'])
Wurde die Option--help
angegeben? Wenn ja wird die Beschreibung mittelsusage(null, parser: parser);
ausgegeben, ohne Fehlermeldung (erster Parameter istnull
). Wir sehen hier, dass auf die Optionen vonresults
mittels der eckigen Klammern zugegriffen werden kann, wie bei einer Map.if (results.rest.isEmpty)
Das Attributrest
der KlasseArgResults
enthält alle Programmargumente, die keine Optionen sind, die also nicht mit '-' anfangen. Diese Liste wird getestet, ob sie leer ist. Wenn ja, liegt der Fehler "zu wenig Argumente" vor, der mit der Funktionusage()
ausgegeben wird.if (testIntArguments(...)
Test, ob die Optionen mit Ganzzahlen korrekt belegt wurden. Siehe Datei helper.dart.&& testRegExpArguments(...)
Test, ob die regulären Ausdrücke korrekt sind. Siehe Datei helper.dart.engine = SearchEngine(...)
Die Suchverarbeitung wird initialisiert...engine.search()
... und durchgeführt.return engine;
Das Ergebnis ist das Objekt mit den Suchergebnissen.
Die Klasse SearchOptions[Bearbeiten]
Diese Klasse speichert die Optionen, die sich auf die Textsuche beziehen.
class SearchOptions { bool count; bool ignoreCase; bool invertMatch; bool list; bool word; String format; String formatContext; int aboveContext; int belowContext; int breakLines; int exitLines; int exitFiles; SearchOptions( {this.count = false, this.ignoreCase = false, this.invertMatch = false, this.list = false, this.word = false, this.format, this.formatContext, this.aboveContext = 0, this.belowContext = 0, this.exitFiles, this.breakLines, this.exitLines}); SearchOptions.fromArgument(ArgResults results) { count = results['count']; ignoreCase = results['ignore-case']; invertMatch = results['invert-match']; list = results['list']; word = results['word']; format = results['format']; formatContext = results['format-context']; aboveContext = intValue(results['above-context']); belowContext = intValue(results['below-context']); final context = intValue(results['context']); if (context > 0) { aboveContext = belowContext = context; } breakLines = intValue(results['break-lines']); exitLines = intValue(results['exit-lines']); exitFiles = intValue(results['exit-files']); } }