Web Dev Solutions

Catalin Mititiuc

Web Log

Elixir, JavaScript, SVG, Containers, Git, Linux

Questions, comments, feedback? Contact the author.

Recursively List All Files In A Directory With Elixir

Introduction

We wish to print a list of all the files in a directory, ignoring files beginning with certain characters, sorted alphabetically, with the directories last.

Finding the files

Code

Finds all files in the path directory recursively, ignoring any files or directories that start with any of the characters in the @starts_with module attribute.

defmodule Files do
  @starts_with [".", "_"]

  def find(path \\ "."), do: find(path, list_contents(path))

  defp find(dir, files) do
    Enum.reduce(files, {dir, []}, fn file, {path, contents} ->
      {path, path |> Path.join(file) |> update_contents(file, contents)}
    end)
  end

  defp list_contents(path), do: path |> File.ls!() |> ignore()

  defp update_contents(path, file, contents) do
    cond do
      File.regular?(path) -> [file | contents]
      File.dir?(path) -> [find(path) | contents]
    end
  end

  defp ignore(filenames) do
    Enum.reject(filenames, &(String.first(&1) in @starts_with))
  end
end

Output

The data structure holding the results of the file search.

iex(1)> directory_tree = Files.find("hello")
{"hello",
 [
   "README.md",
   {"hello/test",
    [
      {"hello/test/support", ["conn_case.ex"]},
      "test_helper.exs",
      {"hello/test/hello_web",
       [{"hello/test/hello_web/controllers", ["error_json_test.exs"]}]}
    ]},
   {"hello/lib",
    [
      "hello.ex",
      {"hello/lib/hello", ["application.ex"]},
      "hello_web.ex",
      {"hello/lib/hello_web",
       [
         {"hello/lib/hello_web/controllers", ["error_json.ex"]},
         "telemetry.ex",
         "router.ex",
         "endpoint.ex"
       ]}
    ]},
   {"hello/priv", [{"hello/priv/static", ["robots.txt", "favicon.ico"]}]},
   {"hello/config",
    ["config.exs", "dev.exs", "test.exs", "prod.exs", "runtime.exs"]},
   "mix.exs"
 ]}

Sorting and printing

Code

Sort alphabetically, with directories last. Downcase before comparing strings.

defmodule Paths do
  def puts(dir_tree), do: dir_tree |> print() |> Enum.join("\n") |> IO.puts()

  defp print({path, contents}), do: path |> list(contents) |> List.flatten()

  defp list(path, contents) do
    contents |> Enum.sort(&alpha_asc_dir_last/2) |> Enum.map(&make_path(&1, path))
  end

  defp alpha_asc_dir_last({a, _}, {b, _}), do: fmt(a) < fmt(b)
  defp alpha_asc_dir_last({_, _}, _), do: false
  defp alpha_asc_dir_last(_, {_, _}), do: true
  defp alpha_asc_dir_last(a, b), do: fmt(a) < fmt(b)

  defp make_path(filename, path) when is_binary(filename), do: Path.join(path, filename)
  defp make_path({path, contents}, _), do: list(path, contents)

  defp fmt(f), do: String.downcase(f)
end

Output

Print all the files sorted.

iex(2)> Paths.puts(directory_tree)
hello/mix.exs
hello/README.md
hello/config/config.exs
hello/config/dev.exs
hello/config/prod.exs
hello/config/runtime.exs
hello/config/test.exs
hello/lib/hello.ex
hello/lib/hello_web.ex
hello/lib/hello/application.ex
hello/lib/hello_web/endpoint.ex
hello/lib/hello_web/router.ex
hello/lib/hello_web/telemetry.ex
hello/lib/hello_web/controllers/error_json.ex
hello/priv/static/favicon.ico
hello/priv/static/robots.txt
hello/test/test_helper.exs
hello/test/hello_web/controllers/error_json_test.exs
hello/test/support/conn_case.ex
:ok

Conclusion

Trying to do this without recursion made it difficult to sort directories first.