File System


There are three modules commonly used together when working in the file system: File, IO (Input/Output), and Path. These modules provide the ability to navigate a filesystem and open, create, modify, copy, read and destroy files. Working with the file system in Elixir is relatively straight-forward. This chapter will focus on the most useful functions with regard to reading and writing data.

As with the typical unix shell, we can navigate around an Elixir project from within iex. I'll start iex from within the root directory of an Elixir application.

# the ls command gives us the directory contents, including hidden files and folders
iex(1)> ls
.gitignore     .vscode        README.md      _build         config         deps
lib            mix.exs        mix.lock       priv           test

# we can change directory with the cd command, passing in the directory
iex(2)> cd "priv"
/Users/Freddy/Elixir Projects/conduit/priv

# now when we list the directory contents we see the contents of the "priv" directory
iex(3)> ls
gettext     repo

# we can print the working directory with pwd
iex(4)> pwd
/Users/Freddy/Elixir Projects/wireshark/priv

# to move backwards, use cd ".."
iex(5)> cd ".."
/Users/Freddy/Elixir Projects/wireshark

Checking if a file exists

We can check if a file exists with the File.exists? function by passing in the name of a file like so:

iex(1)> File.exists? "text_file.txt"
true

# when passing in a single argument the parenthesis are optional

Read a file into memory

One of the most common tasks is reading the contents of a file. The easiest method of doing this is using File.read and passing in the name of the file. The response will either be {:ok, contents} if the operation is successful or {:error, reason} if the operation fails. Since we know there are two possible responses, we can use a case statement to pattern match the return value.

case File.read("text_file.txt") do
  {:ok, contents} -> IO.puts(contents)
  {:error, response} -> IO.puts("The operation failed due to #{reason}")
end

The case statement defines two possible responses. The code that executes depends on which case matches. If the read operation succeeds then the first case matches and the contents are printed to the standard output. If the operation fails the second case will match and the error message with the corresponding reason will be printed.

There is a hidden benefit to using a case statement for File operations. However, in order to understand this benefit we'll first need to explore opening and reading files manually.

Open a file

Another common task is to write to a file. In order to do so, the File module provides an open/2 function. Remember the /2 indicates the function's arity, the number of arguments a function takes. The open/2 function takes the path to the file as the first argument and an optional list of modes as the second argument. A mode is the intended reason for opening a file, given as an atom. For example, we may open a file with File.open("text.txt", [:read, :write]. In this case a successful operation will return {:ok, pid} where "pid" represents the process identifier. This pid is the Elixir process that holds the contents of our file. Since we know the return value, we can pattern match the call to open like so:

iex(1)> {:ok, file} = File.open("text_file.txt", [:read, :write])
{:ok, #PID<0.96.0>}

# variable "file" now contains the pid

We can read the file in its entirety with the IO module like so:

# the option :all will return the contents of the file
iex(2)> IO.read(file, :all)
"This is line one of a text file.\nThis is line two of a text file.\n"

We can read the file line by line with the IO module like so:

iex(3)> IO.read(file, :line)
:eof

# uh oh! what happened?

The response :eof stands for "end of file". The IO module begins reading the file from the last point at which it stopped. Since we read the entire file by passing in :all on line 2, we've reached the end of the file. We'll have to restart iex in order to demonstrate reading the file line by line. Stop iex from running by pressing ctrl + c + c. Let's try again:

iex(1)> {:ok, file} = File.open("text.txt", [:read, :write])
{:ok, #PID<0.89.0>}

iex(2)> IO.read(file, :line)
"This is line one of a text file.\n"

iex(3)> IO.read(file, :line)
"This is line two of a text file.\n"

iex(4)> IO.read(file, :line)
:eof

# as expected, we read the file line by line until reaching :eof

This time we were able to open the file and store its reference in the variable "file". We then read the file line by line until reaching the end. However, we have one more step left. Each file that's opened needs to be closed.

iex(5)> File.close file
:ok

If we forget to close the file, the contents remain in memory taking up space. Remember there was a benefit of using case statements? The do..end block opens the file and then closes it automatically. In this way, we no longer need to remember to close our files manually.

results matching ""

    No results matching ""