Daily Code Reading #13 – Flay#expand_dirs_to_files

I’m looking at flay’s #expand_dirs_to_files today. This method takes a list of files and directories and will recursively walk them to collect all of the Ruby source files that need to be analyzed.

The Code

1
2
3
4
5
6
7
8
9
10
11
  def self.expand_dirs_to_files *dirs
    extensions = ['rb'] + Flay.load_plugins
 
    dirs.flatten.map { |p|
      if File.directory? p then
        Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
      else
        p
      end
    }.flatten
  end

Review

#expand_dirs_to_files has four steps:

  1. Collect all of the file extensions
  2. Iterative over each item in the dirs list
  3. If the item is a directory, find all files inside. Otherwise just list the item
  4. Collect and flatten all of the files that were found

1. Collect all of the file extensions

1
extensions = ['rb'] + Flay.load_plugins

Flay builds an array of extensions here. What’s interesting to me is that this gets sent to File#join separated by commas:

1
2
3
4
5
# Source
File.join(p, '**', "*.{#{extensions.join(',')}}")
 
# Expanded with extensions = ['rb', 'liquid', 'xml']
File.join(p, '**', "*.{rb,liquid,xml}}")

It doesn’t look like the {} syntax is documented in File#join at all.

Update: Gregor Schmidt explained in the comments below that {} is documented in Dir#glob. So File#join assembles a string like ./**/*.{rb,liquid,xml} which gets passed into Dir#[] below.

Something else I didn’t know was that flay supports plugins. An ERB plugin is included with Flay but it looks like it might be easy to write plugins for other Ruby languages like HAML or Liquid.

2. Iterative over each item in the dirs list

1
dirs.flatten.map { |p| ... }

This is just a simple iterator and since it’s the last statement in #expand_dirs_to_files it will return the array to the caller.

3. If the item is a directory, find all files inside. Otherwise just list the item

1
2
3
4
5
if File.directory? p then
  Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
else
  p
end

Here is where the files and directories are recursively walked to find all files with matching extensions. I like how this code itself isn’t recursive, but it makes use of Dir#[]‘s recursive globs to find all files in sub-directories (using ‘**/’).

4. Collect and flatten all of the files that were found

To finish up the search, #expand_dirs_to_files flattens the results into a single array and then returns it to the caller.

It’s interesting how #expand_dirs_to_files made use of File#join and Dir#[] to do all of the file searching, but still has a nice and simple public API. This pattern would be useful anytime I need to recursively search for files in Ruby.