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!