The power of the unix philosphy is that you can compose single-purpose tools together, to great effect. For example, let’s say we are working on a Rails app. We want to find all controllers that use current_user, and also inherit from ApplicationContrller.

I would run the following:

$ join \
 <(rg '< ApplicationController' app/controllers --files-with-matches | sort) \
 <(rg 'current_user' app/controllers --files-with-matches | sort)

Let’s break it down.

$ rg '< ApplicationController' app/controllers --files-with-matches

ripgrep (rg) is an excellent replacement for grep: It searches the content of files for a matching regex. The expression above searches for the regex < ApplicationController to find classes inheriting from ApplicationController inside the app/controllers directory. Like grep, rg can return both file, line number and match information. In this case, I am directing it only return filenames with --files-with-matches.

We now have a list of files in app/controllers that have classes inheriting from ApplicationController.

Next, we want to find uses of current_user in those files. There are a few ways we can accomplish that. I decided to find all controllers that use current_user, and later compare the two lists. The second list of files is found with:

$ rg 'current_user' app/controllers --files-with-matches

With the two lists in hand, we can then turn to join. From the man page:

join – relational database operator

The join utility performs an ``equality join’’ on the specified files and writes the result to the standard output.

Much like a JOIN in SQL which find corresponding records in two tables, join can find matching records in two files. Its usage typically requires specifying which field in each line to use for the join. Our usage is very simple though: Our lists of files only have a single field: The file name.

join expects two files as arguments. We could redirect the output of each of our rg calls to a file, and use those files as input to join. However, bash (and other shells too) allow for process substitution: It can take care of presenting the output of a subprocess to another process as if it was a file. That is done via the <() syntax, used twice: Once for each rg search.

The last bit is the usage of sort. join expects the files to be sorted:

When the default field delimiter characters are used, the files to be joined should be ordered in the collating sequence of sort(1)

And there it is! We used rg, sort, join, and a bit of bash plumbing to find files that have lines matching two different regexes:

$ join \
 <(rg '< ApplicationController' app/controllers --files-with-matches | sort) \
 <(rg 'current_user' app/controllers --files-with-matches | sort)