Summary: I wanted some specific features in a “find” utility, and when I couldn’t figure out how to get them with combinations of find
, awk
, and other Unix commands, I wrote what I wanted in Scala. Those features are (a) showing matching filenames, (b) showing the line that matches my search pattern, and underlining the pattern in the output, (c) showing the line numbers of the matches, and (d) showing an optional number of lines from the file before and after each match.
Introduction
The background of this particular effort is that I’ve wanted my own custom search tool for a while. I was motivated to create it this week because I’m currently working with Flutter (and therefore Dart), and I wanted a tool that would let me search hundreds (or thousands) of Flutter/Dart example files. So I created a tool I named ff
, for “file find.”
As an example of the typical use case, I vaguely remembered that you can write text to a clipboard using Flutter with a Clipboard
widget, so to find Clipboard
examples I issue this ff
command:
ff -d /Users/al/Projects/Flutter -f "*.dart" -p Clipboard -b 1 -a 1
That command currently produces this output:
That image shows all of the features I want in my utility:
- The matching filenames are shown
- The matching line numbers are shown
- It highlights my pattern in the search results (currently underlining it)
- Because I used
-b 1 -a 1
, it shows one line before and one line after each pattern match
Here’s that image again with those features highlighted:
Before and after are optional
Printing lines before and after the search results (using -b
and -a
) are optional. The previous command and output looks like this when those options are omitted:
Using regular expressions
At the moment you can search for regular expression patterns, but:
- Your regex must match the entire line
- The output is currently a little garbled (because of my underlining algorithm)
As an example of the regex issue, if you want to search for the word delegate
followed later on that line by the word SlidableDrawer
, this regex will not work:
ff -d /Users/al/Projects/Flutter -f "*.dart" -p "delegate.*SlidableDrawer"
The problem is that the regex needs to match the entire line, so the correct approach is to use this regex:
ff -d /Users/al/Projects/Flutter -f "*.dart" -p ".*delegate.*SlidableDrawer.*"
In regards to the underlining issue, it’s a bug that I’ll look into when I have some more free time.
Usage
You can see the options for the command by typing ff
by itself:
$ ff
Usage: ff [options]
-d, --dir <value> required; the directory to search
-p, --search-pattern [searchPattern] (like 'StringBuilder' or '^void.*main.*')
required; regex patterns must match the full line
-f, --filename-pattern [filenamePattern] (like '*.java')
required; the filenames to search
-b, --before [before] (the number of lines BEFORE the search pattern to print, like 1 or 2)
-a, --after [after] (the number of lines AFTER the search pattern to print, like 1 or 2)
All functionality related to command-line arguments, including this output, comes from using the scopt project.
The future: one more feature
One more feature I’d like to add in the future is the ability to find all files that contain multiple patterns. For example, when I want to see how to use the onTap
event with a ListTile
, it would be nice to be able to say, “Find all instances of ListTile
where onTap
occurs within 10 lines of ListTile
.”
Right now I can solve that problem by searching for ListTile
, adding -a 10
, and then looking through the results manually, like this:
$ ff -d /Users/al/Projects/Flutter -f "*.dart" -p ListTile -a 10
(lots of output skipped here ...)
/Users/al/Projects/Flutter/PracticalFlutterBook/ch_05+06/flutter_book/lib/notes/NotesList.dart
----------------------------------------------------------------------------------------------
67: child : ListTile(
68: title : Text("${note.title}"),
69: subtitle : Text("${note.content}"),
70: // Edit existing note.
71: onTap : () async {
72: // Get the data from the database and send to the edit view.
73: notesModel.entityBeingEdited = await NotesDBWorker.db.get(note.id);
74: notesModel.setColor(notesModel.entityBeingEdited.color);
75: notesModel.setStackIndex(1);
76: }
77: )
While this manual approach is certainly doable, I don’t think it’s too hard to implement the search part of this, so I’d like to add it. (The bigger question at the moment is, “What does the UI look like for this?”)
Disclaimer
I wrote this code in about six hours, so I’m sure in a few days I’ll look back at it and say, “OMG, what was I thinking?” :)
Also, as an acknowledgment, the Finder
class is pretty much a straight Scala port of the Java class on this Oracle page.
The end
I could write more, but I hope that’s enough to see whether this is something of interest to you. If you’re interested in more details, you can find the source code for my ff
project here:
The project’s README file supplies a little more information, such as how I build the project with sbt-assembly and GraalVM.