Elixir Tricks: Building a Recursive Function to List All Files in a Directory

Let's use Elixir to build a simple function to list out all of the files in a directory with subdirectories.

We'll need to list out all of the files and folders in the top-level directory and iterate over them. If the item we are iterating over is a file, we'll collect it into a list. If it's a directory, we'll list out all of the files within it, iterate over them and preform the same actions of checking to see if the item we are iterating over is a file or a directory. It's a perfect scenario for recursion.

Let's get started!

defmodule FlatFiles do 
  def list_all(filepath) do
    _list_all(filepath)
  end

  defp _list_all(filepath) do 
    cond do
      String.contains?(filepath, ".git") -> []
      true -> expand(File.ls(filepath), filepath)       
    end
  end

  defp expand({:ok, files}, path) do
    files
    |> Enum.flat_map(&_list_all("#{path}/#{&1}"))
  end

  defp expand({:error, _}, path) do 
    [path]
  end
end

We have a public function, list_all, which takes in an argument of the filepath to the directory whose files we want to list.

The list_all function calls a private function, _list_all, which does the real work for us.

The first thing _list_all does is to determine whether or not the filepath we are iterating over contains .git. I don't want to collect the files in a .git directory, if one is present, so we'll return an empty list if so.

Otherwise, we'll call a helper function, expand.

The expand function takes in an argument of the top-level filepath and the result of calling:

File.ls(filepath)

This will return one of two things. If the filepath represents a path to a directory, it will return:

{:ok, files}

In which files is a list of the files in the directory.

If the filepath represents a path to a file, it will return:

{:error, ::enotdir}

We'll use Elixir's nifty pattern matching to handle these two cases.

If the filepath we are listing is a directory:

def expand({:ok, files}, path) do
  files
  |> Enum.flat_map(&_list_all("#{path}/#{&1}"))
end

We'll use Enum.flat_map to iterate over the files we listed, yielding each one back to our _list_all function.

_list_all will repeat the previous step of calling expand with the result of File.ls(filepath).

This will repeat until the filepath represents a path to a file, not a directory, in which case the other version of our expand function will be invoked.

def expand({:error, _}, path) do 
  [path]
end

This version of our expand function will yield the path, i.e. the path to our single file, inside of a list.

We want to yield back the path enclosed in a list because our call to Enum.flat_map will build a new enumerable by:

  • Invoking a function on each member of the enumerable passed as it's first argument.
  • Appending the return value of each function invocation to the new enumerable it is building.

To use Enum.flat_map, the function that we give it to invoke on each member element must return an enumerable. For this reason, our expand(:error, _}, path) function must return the filepath as a list.

This is also why the condition in our _list_all function that checks for a .git directory returns an empty list. Flat map will collect these return values, either the list containing our single filepath or the empty list, and append them to the new list it is building, thereby creating one flat list of all of the desired files in all subdirectories of our given directory.

And that's it!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus