Datei search engine.dart: Unterschied zwischen den Versionen

Aus Info-Theke
Zur Navigation springen Zur Suche springen
 
(3 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 470: Zeile 470:
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 481: 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.
Zeile 495: 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 {
  if (results.arguments.length < 2) {
    final results = parser.parse(arguments);
     usage('too few arguments', parser: parser);
     final intArgs = [
  } else if (results['help']) {
      'above-context',
    usage(null, parser: parser);
      'context',
  } else if (testIntArguments(
      'below-context',
          results,
      'break-lines',
          [
      'exit-lines',
            'above-context',
      'exit-files',
            'context',
      'verbose-level'
            'below-context',
    ];
            'break-lines',
    if (results['help']) {
            'exit-lines',
      usage(null, parser: parser);
            'exit-files',
    } else if (results.rest.isEmpty) {
            'verbose-level',
        usage('too few arguments');
          ],
    } else if (testIntArguments(results, intArgs, usage) &&
          usage) &&
        testRegExpArguments(results, ['excluded', 'excluded-dirs'], usage)) {
      testRegExpArguments(
      if (results.rest.isEmpty) {
          results,
        usage('too few arguments', parser: parser);
          [
      } else {
            'excluded',
        engine = SearchEngine(results.rest[0],
            'excluded-dirs',
            results.rest.length == 1 ? ['.'] : results.rest.sublist(1),
          ],
            searchOptions: SearchOptions.fromArgument(results),
          usage)) {
            verboseLevel: intValue(results['verbose-level']),
    if (results.rest.isEmpty) {
            fileOptions: FileOptions.fromArgument(results));
      usage('too few arguments', parser: parser);
        engine.search();
    } 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 541: 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> benutzt? Wenn ja wird die Beschreibung mittels <code>usage(null, parser: parser);</code> ausgegeben, ohne Fehlermeldung (erster Parameter ist <code>null</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 (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.
* <code>if (testIntArguments(...)</code> Test, ob die Optionen mit Ganzzahlen korrekt belegt wurden. 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]].

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 Attribut usage der Klasse ArgParser 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 Klasse ArgResult aus dem externen Paket args.
  • recursive = results['recursive']; Das boolsche Attribut recursive wird direkt aus dem Objekt der Klasse ArgResult übernommen. Wir lernen: Zugriff auf die Optionen erfolgt mittels Namen recursive 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 als static 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 Dateimustern filePatterns, Dateioptionen, Suchoptionen und ein Level für Meldungen verboseLevel ü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 Format null 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 Variable line2 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 Ziffer 1-9 liegt.
      • Die Klasse RegExp liefert mit der Methode allMatches() nacheinander alle Treffer des regulären Ausdrucks in dem als Parameter übergebenen String (hier format) liefert. Über diese Treffer wird die Schleife gebildet.
    • final placeholder = match2.group(0); Der Treffer (beispielsweise %f%) wird in der Variablen placeholder 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 Objekt match vor dem Operator null ist, sondern das Ergebnis von match?.group(0) ist dann null. 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 aus true false macht und umgekehrt.
  • 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 Pfaden paths 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 Dateinamensmuster filePattern zerlegt.
    • In die Pfadliste wird das aktuelle Verzeichnis '.' eingetragen, wenn directory 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 in regExp = RegExp(pattern2,...) inkorrekt ist. Es wird dann eine Fehlermeldung mittels der Funktion usage() 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 Klasse File> instantiiert ein Objekt mit dem Dateinamen file, die Methode readAsLinesSync() liefert den Dateiinhalt als Liste von Zeilen.
  • for (var line in lines) iteriert über alle Zeilen
  • final match = regExp.firstMatch(line); Test, ob die Zeile line 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.
  • if (!isHit) Wenn kein Treffer vorliegt, Behandlung des "nachfolgenden Contexts" (Option --above-lines), am Ende ein continue;, damit die Schleife mit dem nächsten Zeile weitermacht.
  • if (searchOptions.list) Wenn die Option --list gesetzt ist, wird der Dateiname ausgegeben showLine(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 ausgegeben
  • if (searchOptions.exitLines != null && totalHitLines >= searchOptions.exitLines) Wenn die Option --exit-lines gesetzt ist und die Zahl der Trefferzeilen erreicht ist, Schnellabbruch mit der Ausnahme ExitException.
  • 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 Ausnahme FileSystemException 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 Methode listSync() eine Liste von File-Objekten ab, die mit der Schleife abgearbeitet wird.
  • name = file.path; Das Attribut path liefert den vollen Namen der Datei.
  • node = path.basename(name); Die Funktion basename() 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 mit continue; 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 der File-Liste weiter.
  • 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).
  • 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 Funktion isBinary 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 Ausnahme ExitException getätigt.
  • try { ... } on FileSystemException { Beim Aufruf der Methode listSync() kann ein Problem auftreten, das das Werfen der Ausnahme FileSystemException 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 Schleife for (var subDir in subDirectories) abgearbeitet. Das geschieht durch Aufruf der Methode searchFilePattern(), also einem rekursiven Aufruf von "sich selber". Die Verschachtelungstiefe depth erhöht sich dann um 1.

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 namens count, 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 Attribut usage der Klasse ArgParser 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 Parameter negatable 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 namens excluded. 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); und addOptions(parser); fügen die Optionen dazu.
  • results = parser.parse(arguments); Die Verarbeitung der Programmargumente wird erledigt, Ergebnis ist ein Objekt vom Typ ArgResults.
  • if (results['help']) Wurde die Option --help angegeben? Wenn ja wird die Beschreibung mittels usage(null, parser: parser); ausgegeben, ohne Fehlermeldung (erster Parameter ist null). Wir sehen hier, dass auf die Optionen von results mittels der eckigen Klammern zugegriffen werden kann, wie bei einer Map.
  • if (results.rest.isEmpty) Das Attribut rest der Klasse ArgResults 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 usage() 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']);
  }
}