miti.sh/docs/2023-08-03-recursively-list-all-files-in-a-directory-with-elixir.md

133 lines
3.3 KiB
Markdown

## 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.