In the previous section where I discussed how file I/O works in Elixir, we got to see a few functions from the File module, which allowed us to open a file for I/O operations. Most of the functions in the File module, however, don't have anything to do with file I/O. Instead, they allow us to interact with the file system, with the names of the functions being very similar to commands on a Unix command line.

Today we're going go through the functions in the File module, looking at each one in some detail. Fortunately, most of these functions are fairly simple, and we should be able to cover them quickly.

Almost every function in the File module has a non-exception-throwing and an exception-throwing version. I'll just cover the non-exception-throwing functions unless the exception-throwing version works differently.

File.cwd/0

The File.cwd/0 function gets the current working directory. Apparently, an Elixir process, like most OS processes, have a concept of a current working directory.

The documentation warns us that errors can result from insufficient read permissions on any parent directories. This is rare and annoying to try and reproduce, so I won't bother.

[Recreate example on a Unix OS]

iex> File.cwd()
{:ok, "c:/Development/Elixir/lwmelixir"}

File.cd/1

The File.cd/1 function changes the current working directory. It's the equivalent to the cd command on the command line.

iex> File.cd("projects")
:ok
iex> File.cd("..")
:ok
iex> File.cd("nonexistent")
{:error, :enoent}
iex>

File.cd!/2

The File.cd!/2 changes the current directory to a particular path, runs a function within the context of that directory, and then switches back to the previous path. This is useful if you just want to temporarily change the directory for the purposes of running a particular bit of code.

iex> File.cd!("./projects", fn -> File.cwd() end)
{:ok, "/home/kevin/Dropbox/Development/lwmelixir/projects"}
iex> File.cwd()
{:ok, "/home/kevin/Dropbox/Development/lwmelixir"}
iex> File.cd!("./projects", fn -> 3 end)
3

I notice that the File.cd!/2 returns whatever the parameter function returns, so it's easy to return data from the temporary context.

File.chgrp/2

The File.chgrp/2 function changes the group ID for a particular file. This is equivalent to the chgrp command on the command line.

iex> File.chgrp("text.txt", 2)
:ok

I was curious what this did on Windows, so I tried it, As far as I can tell, running this on Windows has no effect. Windows has a different concept of groups than a Unix OS.

File.chmod/2

The File.chmod/2 function changes the permissions on a particular file. The is the equivalent of the chmod command on the command line.

Like with the chmod Unix command, the File.chmod function receives an octal number with three digits. The three bytes in each octal digit represent read, write, and execute permissions for "owner", "group", and "other". That's why the number in the second parameter starts with 0o: that indicates that the number is an octal number.

iex> File.chmod("text.txt", 0o622)
:ok
iex> File.chmod("text.txt", 0o000)
:ok

I'm not sure exactly what the consequences are in Windows, since chmod is a Unix concept. I was able to make files read-only in Windows, but I didn't notice any other effects.

File.chown/2

The File.chown/2 function changes the owner of a file to a particular user ID. The is the equivalent of the chown command on the command line.

iex> File.chown("text.txt", 100)
:ok

Since chown is derived from a Unix tool, I'm not sure what effect it has in Windows. When trying it out in Windows, I did not notice any effect.

File.close/1

The File.close/1 function closes a file device. The documentation warns us that if the file was opened with the :delayed_write option, some writes to the file may occur when closing it.

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

File.copy/3

The File.copy/3 function copies the bytes in a file from a source file to a destination file. With this function, the source and the destination can also be IO devices, so you could copy some file contents to other IO devices or vice versa. This function does a low-level copy of the bytes in the file, and does not in any way attempt to interpret text encoduings.

The first parameter is the source file/device, the second parameter is the destination file/device, and the third parameter is an optional parameter which controls the number of bytes that are copied. By default, the third parameter has a value of :infinity, which means that all the bytes are copied.

The function return a tuple that contains the result code (:ok or :error) for the first value and the number of bytes copied (when successful) or a reason for the error (when unsuccessful).

iex> File.copy("text.txt", "text_copy.txt")
{:ok, 47}

That copied the entire text file, which turned out to be 47 bytes. Here's the listing of the contents of the copied file, which is the example text file from lwm 74.

> cat text_copy.txt
This is an example
of a multi-line
text file.

This time, I'm only going to copy 25 bytes.

iex> File.copy("text.txt", "text_copy.txt", 25)
{:ok, 25}

Here are the copied file contents now.

> cat text_copy.txt
This is an example
of a

I also tried copying the text file to the standard out (:stdio) and standard error (:stderr) devices.

iex> File.copy("text.txt", :stdio)
{:ok, 47}
iex> File.copy("text.txt", :stderr)
{:ok, 47}

What happened is that files were created called "stdio" and "stderr" and the contents were written to those files. I'm guessing that this function doesn't understand the atoms that represent standard devices.

File.cp/3

The File.cp/3 function copies a file. This is the equivalent to the cp command on the command line. Unlike File.copy/3, this function only takes path strings and copies the entire contents of the file.

The first parameter is the source file, the second parameter is the destination file, and the third optional parameter is a function that determines if the destination file will be overwritten. The third parameter function is called when the destination file already exists. If it returns true, the destination is overwritten. If it returns false, the destination is not overwritten. The default function returns true, so the destination is overwritten unless you supply a different function.

The third parameter function accepts two parameters, but the documentation does not specify what those parameters are. I looked into the Elixir source code, and it turns out that they are just the source and destination file paths that you passed to File.cp/3.

iex> File.cp("text.txt", "text_copy.txt")
:ok
iex> File.cp("text.txt", "text_copy.txt", fn _, _ -> false end)
:ok
iex> File.cp("text.txt", "text_copy.txt", fn source, destination ->
...> IO.puts(source)
...> IO.puts(destination)
...> true
...> end)
text.txt
text_copy.txt
:ok

The first example copies one file to another, overwriting any file that is already there. The second example does the same copy operation, but indicates that if the destination file exists, it should not be overwritten. The third example is the same as the first example, but it prints the two parameters that are passed to the parameter function, showing that they are indeed the source and destination.

File.cp_r/3

The File.cp_r/3 function works just like File.cp/3, except that it can copy directories as well as files. If the source is a directory, it will be copied recursively, meaning all of its contents will be copied. This function will fail if you attempt to copy a file to a destination that is a directory and vice versa.

If successful, the function will return a tuple containing :ok and a list of the files that were copied.

#Copy a text file to another
iex> File.cp_r("text.txt", "text_copy.txt")
{:ok, ["text_copy.txt"]}
#Copy a subdirectory to another
iex> File.cp_r("subdir", "subdir_copy")
{:ok,
 ["subdir_copy/subsubdir/fileB.txt", "subdir_copy/subsubdir/fileA.txt",
  "subdir_copy/subsubdir", "subdir_copy/file2.txt", "subdir_copy/file1.txt",
  "subdir_copy"]}
#Copy an empty subdirectory to another
iex> File.cp_r("empty_dir", "empty_dir_copy")
{:ok, ["empty_dir_copy"]}
#Attempt to copy a file to a path that is already occupied by a directory
iex> File.cp_r("text.txt", "subdir")
{:error, :eisdir, "text.txt"}
#Attempt to copy a directory to a path that is already occupied by a file
iex> File.cp_r("subdir", "text.txt")
{:error, :enoent, "subdir/file1.txt"}
#Copy a file, but don't overwrite the destination. No files are copied.
iex> File.cp_r("text.txt", "text_copy.txt", fn _, _ -> false end)
{:ok, []}

I inserted comments above each example to indicate what it is demonstrating.

File.dir?/2

The File.dir?/2 function returns true if a path matches a directory, otherwise it returns false. That's pretty easy. We can test if any path corresponds to a directory.

#Check if a file is a directory
iex> File.dir?("text.txt")
false
#Check if a directory is a directory
iex> File.dir?("subdir")
true
#Check if a non-existent path is a directory
iex> File.dir?("nonexistent")
false

The documentation indicates that you can pass a :raw option if you want to only check for the directory locally and bypass the file server. I have no idea what this means, but I suspect it applies to when you have an Elixir cluster that forms a distributed system. Perhaps it has a distributed file system as well?

File.exists?/2

The File.exists?/2 function checks whether the given path exists. The path can identify a file, directory, socket, symbolic link, named pipe, or device file. I've never seen a socket or a named pipe in a file system before, but I'm far from a Unix OS expert. I guess they can be represented as paths as well.

#Check if a file exists
iex> File.exists?("text.txt")
true
#Check if a directory exists
iex> File.exists?("subdir")
true
#Check if a non-existent path exists
iex> File.exists?("nonexistent")
false

File.ln/2

The File.ln/2 function creates a hard link to an existing file. This function will return an error if the operating system does not support hard links.

Here are some examples of File.ln/2 being run.

iex> File.ln("text.txt", "text_link.txt")
:ok
iex> File.ln("nonexistent.txt", "nonexistent_link.txt")
{:error, :enoent}

File file "text_link.txt" is created as a hard link to "text.txt". Unlike symbolic links, hard links will still refer to the original content when the original file is deleted. You can think of hard link (and the original file) as equal-status file descriptors that all point to the same content. The content is only deleted when the last file descriptor is deleted.

To my surprise, this function also worked on Windows. Apparently the NTFS file system does support hard links, which I didn't know until I looked into it.

File.ln_s/2

The File.ln_s/2 function creates a symbolic link to an existing file. This function will return an error if the operating system does not support symbolic links.

A symbolic link is just a tiny file that points to another file. If you delete the file it's pointing to, the link will be invalid and you'll get error while trying to use it.

Here are some examples of File.ln_s/2 being run.

iex> File.ln_s("text.txt", "text_symblink.txt")
:ok
iex> File.ln_s("nonexistent.txt", "nonexistent_link.txt")
:ok

Notice that I was able to create a symbolic link to a non-existent file. The link won't work, but it can be created.

> ls -l
total 15

lrwxrwxrwx 1 kpeter 1049089   15 May 15 12:14 nonexistent_link.txt -> nonexistent.txt
-rw-r--r-- 2 kpeter 1049089   47 Apr 24 11:25 text.txt
-rw-r--r-- 2 kpeter 1049089   47 Apr 24 11:25 text_link.txt
lrwxrwxrwx 1 kpeter 1049089    8 May 15 12:13 text_symblink.txt -> text.txt

I can list the contents of the valid symbolic link, but not the invalid symbolic link.

> cat text_symblink.txt
This is an example
of a multi-line
text file.
> cat nonexistent_link.txt
cat: nonexistent_link.txt: No such file or directory

File.ls/1

The File.ls/1 function returns a list of strings that represent all the files and directories in a particular path. This is the equivalent of the ls command on the command line, which lists the contents of a directory. Unlike the ls command, there don't appear to be any options you can pass to File.ls/1.

The only parameter is the path of the directory whose contents you want to retrieve. This is an optional parameter, and the default value is ".", which is the current working directory.

iex> File.ls()
{:ok,
 ["blob.bin", "empty_dir", "example.txt", "food.txt", "nom_nom_io.exs",
  "nom_nom_stream.exs", "nonexistent_link.txt", "subdir", "subdir_copy",
  "text.txt", "text_copy.txt", "text_link.txt", "text_symblink.txt"]}
iex> File.ls("subdir")
{:ok, ["file1.txt", "file2.txt", "subsubdir"]}
iex> File.ls("nonexistentdir")
{:error, :enoent}

Here's the equivalent ls command on the command line.

> ls
blob.bin    example.txt  nom_nom_io.exs      nonexistent_link.txt@  subdir_copy/  text_copy.txt  text_symblink.txt@
empty_dir/  food.txt     nom_nom_stream.exs  subdir/                text.txt      text_link.txt
> ls subdir
file1.txt  file2.txt  subsubdir/

> ls nonexistentdir
ls: cannot access 'nonexistentdir': No such file or directory

File.lstat/2

The File.lstat/2 function retrieves the metadata for a path, which could identify a directory or file. The first parameter is the path and the second parameter are some options, which configure how the timestamps are displayed.

Here are examples of retrieving the metadata for a file, a directory, and then exploring the timestamp options.

iex> File.lstat("text.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 4, 24}, {18, 24, 47}},
   ctime: {{2019, 4, 24}, {18, 24, 47}},
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 4, 24}, {18, 25, 10}},
   size: 47,
   type: :regular,
   uid: 0
 }}
iex> File.lstat("subdir")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 14}, {21, 0, 40}},
   ctime: {{2019, 5, 14}, {20, 59, 47}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 16895,
   mtime: {{2019, 5, 14}, {21, 0, 40}},
   size: 0,
   type: :directory,
   uid: 0
 }}
iex> File.lstat("text.txt", [time: :local])
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 4, 24}, {11, 24, 47}},
   ctime: {{2019, 4, 24}, {11, 24, 47}},
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 4, 24}, {11, 25, 10}},
   size: 47,
   type: :regular,
   uid: 0
 }}
iex> File.lstat("text.txt", [time: :universal])
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 4, 24}, {18, 24, 47}},
   ctime: {{2019, 4, 24}, {18, 24, 47}},
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 4, 24}, {18, 25, 10}},
   size: 47,
   type: :regular,
   uid: 0
 }}
iex> File.lstat("text.txt", [time: :posix])
{:ok,
 %File.Stat{
   access: :read_write,
   atime: 1556130287,
   ctime: 1556130287,
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: 1556130310,
   size: 47,
   type: :regular,
   uid: 0
 }}

It looks like the timestamp is :universal (in UTC time) by default.

File.mkdir/1

The File.mkdir/1 function creates a directory with the given path. If any parent directories do not exist, this function will fail. There are also a variety of other reasons this function can fail, which are listed in the function documentation. This is the equivalent to the mkdir command on the command line.

iex> File.mkdir("./newdir")
:ok
iex> File.mkdir("./nonexistent/newdir")
{:error, :enoent}

The last example failed because the directory "nonexistent" didn't already exist.

File.mkdir_p/1

The File.mkdir_p/1 function works the same was as File.mkdir/1, except that it will automatically create any non-existent parent directories. This is the equivalent to the mkdir -p command on the command line.

iex> File.mkdir_p("./newdir")
:ok
iex> File.mkdir_p("./nonexistent/newdir")
:ok

This time both examples succeeded. The "nonexistent" directory did not exist, but was automatically created, and then "newdir" was created inside of it.

File.open/2

The File.open/2 function opens a file and gives us a device representing that file. Internally, Elixir manages each open file in a separate process, and any interaction with the file is implemented by exchanging data with the file process.

There are essentially two ways to use File.open/2. The first way, which is probably the more common way, is to pass a file path as the first parameter and some options into the second parameter.

The options indicate how a file will be opened. The documentation lists most of the options, but you have to do a bit of digging to find all the possible options, so I'm going to list them here.

Option Description
:binary Opens a file in binary mode. The file will be regarded as a blob of bytes and any text encodings will be ignored.
:read The file is being opened for reading
:write The file is being opened for writing.
:append The file is being opened for writing, and the file will be appended to instead of being overwritten
:exclusive The file is opened for writing, but only if it doesn't already exist. If it exists, an error will be returned.
:charlist When the file is being read, data will be returned in the form of character lists instead of the usual binaries.
:compressed Allows a gzipped file to be read or written to
:utf8 Opens a file in text mode. The text encoding is UTF-8.
:read_ahead Does buffered reading, which improves read performance since a block of bytes/characters is read whenever a read operation takes place, even if you've only requested a single byte. This means that you don't have to wait for disk I/O every time you want to read something because you have an in-memory buffer you can read from. The Erlang documentation states that the default buffer size is around 64KB.
{:read_ahead, <>} Does buffered reading, but provides more control. The integer value specifies the size of the read buffer, which is filled up by the read operation reading the next N bytes, even if you only need a single byte.
:delayed_write The is the write-equivalent of :read_ahead. All writes are written to an in-memory buffer, which is eventually written to disk. This improves performance (at a cost of a little memory usage) by reducing the disk I/O.
{:delayed_write, <>, <>} Does buffered writing, but provides more control. The first integer is the size of the buffer and the second integer is a number of milliseconds. The contents of the write buffer will be written to disk after the buffer has been filled up or the specified number of milliseconds has elapsed.
:raw Opens the file in Erlang raw mode, which means that only a few Erlang (not Elixir) functions can be used to read and write. The file is handled by the current process instead of a separate process. This can provide some speed improvements at the cost of tying up the current process and just being really inconvenient to use. See the Erlang documentation for more details.
:ram From what I've gathered from reading the Erlang documentation (this option isn't covered in the Elixir documentation), you can pass some in-memory data into File.open/2 and it will be wrapped in a device that allows us to treat it as a file
:sync This flag enabled POSIX synchronous I/O or its equivalent in other OSs (like Windows). It's unclear from the description, but I think this means that the function will not complete until the write operation has been finished and the data has been written to disk. I personally would not mess with this flag until I understood the full consequences: it sounds like it would rarely be useful.
{:encoding, <>} This option allows you to specify a specific encoding when the file is a text file. The options are :latin1, :utf8, :utf16, and :utf32. The :utf16 and :utf32 encodings have big and little endian variants. In my experience, it's best to just stick to UTF-8 all the time.

Here are some examples of opening some files.

iex> {:ok, file} = File.open("text.txt", [:utf8, :read_ahead])
{:ok, #PID<0.132.0>}
iex> File.close(file)
:ok
iex> {:ok, file} = File.open("write_file.txt", [:binary, :write, :delayed_write])
{:ok, #PID<0.135.0>}
iex> File.close(file)
:ok

The second way to use File.open/2 is to pass it a path as the first parameter and a function as the second parameter. That parameter function receives the open file device as its parameter and it can return anything. Whatever the parameter function returns is then returned from File.open/2. This gives the developer a way to modify or wrap the file device and return something else. You could use this to read a piece of the file and return that instead of the actual file device.

Here's an example.

iex> File.open("text.txt", fn device ->
...> File.close(device)
...> IO.puts("File opened and then closed")
...> :fantastic
...> end)
File opened and then closed
{:ok, :fantastic}

File.open/3

The File.open/3 function is a combination of the two ways to use File.open/2. With File.open/2, you can pass some options or a function as the second parameter. With File.open/3, you must specify both.

Here's an example. I open a file, read the first line from the file, and return it.

iex> File.open("text.txt", [:read, :utf8], fn file ->
...> first_line = IO.read(file, :line)
...> File.close(file)
...> first_line
...> end)
{:ok, "This is an example\n"}

File.read/1

The File.read/1 function reads in the entire contents of a file and stores them in memory. It's a quick and easy way to read a file and store its contents in memory, but I recommend doing so only for smaller files.

Here's an example of using it.

iex> File.read("text.txt")
{:ok, "This is an example\r\nof a multi-line\r\ntext file."}

When there's an error, it returns :error along with an atom giving the reason for the error. The Elixir documentation gives us the following error reason atoms, which it indicates are the most typical reasons that errors occur.

Atom Description
:enoent The file does not exist
:eacces There was a permissions issue that prevented the file from being accessed
:eisdir It's not a file, but a directory
:enomem There's not enough memory to store the contents of the file

File.read_link/1

The File.read_link/1 function takes a path that identifies a symbolic link and returns the path of the file the symbolic link points to. This function will only work on symbolic links, and will return an error if the parameter does not correspond to a symbolic link.

iex> File.read_link("text_symblink.txt")
{:ok, "text.txt"}
iex> File.read_link("text.txt")
{:error, :einval}

File.regular?/2

The File.regular?/2 function takes a path as the first parameter and returns true if the path is a regular file, otherwise false. This function automatically follows symbolic links, so it will evaluate the file the symbolic link is pointing to.

iex> File.regular?("text.txt")
true
iex> File.regular?("subdir")
false
iex> File.regular?("text_symblink.txt")
true

File.rename/2

The File.rename/2 function takes a source path representing a file or directory and a destination path and then changes the file or directory to match the destination path. If that destination path is in the same directory, it has the effect of renaming the file or directory. If the destination path is in a different directory, it has the effect of moving the file or directory.

The Elixir documentation warns that the destination path for a file must be the full path. Unlike the Unix mv command, you can't specify a just destination directory to move the file to: you must specify the directory and the file name in the destination path.

iex> File.rename("text.txt", "more_text.txt")
:ok
iex> File.rename("more_text.txt", "text.txt")
:ok
iex> File.rename("nonexistent.txt", "existent.txt")
{:error, :enoent}
iex> File.rename("text_copy.txt", "./subdir/text.txt")
:ok

I rename "text.txt" to "more_text.txt" and then I rename it back to "text.txt". Then I attempted to rename a non-existent file and got an error. Finally, I moved a text file into a subdirectory and gave it a different name. Checking the file system after each operation showed that the function had its intended effect.

File.rm/1

The File.rm/1 function deletes a file with the given path. This function does not delete directories unless the user is a super user, and it does not do recursive deletion. This is the equivalent of the rm command on the command line.

#Delete a file
iex> File.rm("delete_file.txt")
:ok
#Delete a directory. I can't because I don't have superuser permissions
iex> File.rm("empty_dir")
{:error, :eperm}
#Delete a non-existent file
iex> File.rm("nonexistent.txt")
{:error, :enoent}

Don't worry, there's another function that can easily delete directories, which we'll cover next.

File.rm_rf/1

The File.rm_rf/1 will recursively delete any file or directory at the given path. Symbolic links are deleted, but are not followed. Interestingly, this function will just ignore any non-existent files or directories and will indicate success. This function is the equivalent of the rm -rf command on the command line.

When the function is successful, it returns a tuple with an :ok atom and list containing all the things that were deleted.

#Delete a file
iex> File.rm_rf("delete_file.txt")
{:ok, ["delete_file.txt"]}
#Recursively delete an empty directory
iex> File.rm_rf("empty_dir")
{:ok, ["empty_dir"]}
#Recursively delete a subdirectory with files and a subdirectory
iex> File.rm_rf("subdir")
{:ok,
 ["subdir", "subdir/text.txt", "subdir/subsubdir", "subdir/subsubdir/fileB.txt",
  "subdir/subsubdir/fileA.txt", "subdir/file2.txt", "subdir/file1.txt"]}
#Delete a symbolic link
iex> File.rm_rf("text_symblink.txt")
{:ok, ["text_symblink.txt"]}
#Delete a non-existent file. It indicates success, but indicates that no files were deleted
iex> File.rm_rf("nonexistent.txt")
{:ok, []}

File.rmdir/1

The File.rmdir/1 function deletes a directory at the given path. This is the equivalent of the rmdir command on the command line. This function does not do recursive deletion, so it can only delete an empty directory.

#Attempt to delete a file
iex> File.rmdir("delete_file.txt")
{:error, :eio}
#Delete an empty directory
iex> File.rmdir("empty_dir")
:ok
#Attempt to delete a subdirectory with contents
iex> File.rmdir("subdir")
{:error, :eexist}
#Attempt to delete a non-existent directory
iex> File.rmdir("nonexistentdir")
{:error, :enoent}

File.stat/2

The File.stat/2 function is very similar to File.lstat/2 in that both functions return metadata for a file or directory. The only difference is that File.lstat/2 will provide metadata about a symbolic link, whereas File.stat/2 will provide metadata about the file that symbolic link points to.

#Retrieves metadata for the symbolic link
iex> File.lstat("text_symblink.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {18, 17, 39}},
   ctime: {{2019, 5, 17}, {18, 17, 39}},
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {18, 17, 39}},
   size: 0,
   type: :symlink,
   uid: 0
 }}
#Retrieves metadata for the file the symbolic link points to, which is text.txt
iex> File.stat("text_symblink.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 4, 24}, {18, 24, 47}},
   ctime: {{2019, 4, 24}, {18, 24, 47}},
   gid: 0,
   inode: 0,
   links: 2,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 4, 24}, {18, 25, 10}},
   size: 47,
   type: :regular,
   uid: 0
 }}

File.stream!/3

The File.stream/3 function creates a stream that can either read from or write to the file. I already covered this function pretty well in lwm 74, where I discussed how file streams work. I'm won't go into any more detail here, but I will provide a brief example.

I'm going to create a stream for the "text.txt" file, read it line by line, converting each line to upper case, and then putting the result into a list.

iex> File.stream!("text.txt", [:utf8], :line) |>
...> Stream.map(fn line -> String.upcase(line) end) |>
...> Enum.to_list()
["THIS IS AN EXAMPLE\n", "OF A MULTI-LINE\n", "TEXT FILE."]

File.touch/2

The File.touch/2 function updates the modification and access times on a file. If the file being specified does not exist, it is created. This is the equivalent to the touch command on the command line. The second parameter is an optional parameter where you can pass it an Erlang datetime or a POSIX timestamp. By default, the current date and time is used.

Here's an example of updating the timestamps on an existing file.

#Retrieve the metadata for a file, showing the timestamps
iex> File.stat("some_file.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 38, 5}},
   ctime: {{2019, 5, 17}, {19, 38, 5}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 38, 5}},
   size: 0,
   type: :regular,
   uid: 0
 }}
#Update the timestamps
iex> File.touch("some_file.txt")
:ok
#Show that the timestamps were updated
iex> File.stat("some_file.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 40, 54}},
   ctime: {{2019, 5, 17}, {19, 40, 54}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 40, 54}},
   size: 0,
   type: :regular,
   uid: 0
 }}

Here's an example of creating a new file.

#Get the metadata of a non-existent file
iex> File.stat("nonexistentfile.txt")
{:error, :enoent}
#Use touch to create a new empty file
iex> File.touch("nonexistentfile.txt")
:ok
#Retrieve the metadata for the file
iex> File.stat("nonexistentfile.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 42, 12}},
   ctime: {{2019, 5, 17}, {19, 42, 12}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 42, 12}},
   size: 0,
   type: :regular,
   uid: 0
 }}

File.write/3

The File.write/3 function writes a binary or string into a file in a single operation. This is similar to File.read/2, but it writes instead of reads. It also has similar options to File.open/2 and File.read/2

Let's use it to write a string to a file.

iex> text = "Badger Badger Badger\nMushroom Mushroom"
"Badger Badger Badger\nMushroom Mushroom"
iex> File.write("badger.txt", text, [:utf8])
:ok

Then we verify that the text was correctly written.

> cat badger.txt
Badger Badger Badger
Mushroom Mushroom

File.write_stat/3

The File.write_stat/3 function updates the metadata on a file. The first parameter is the path, the second parameter is the metadata to be written, and the third (optional) parameter is the timestamp options, which I discussed in the section on File.lstat/2. The metadata is the same File.Stat structure that both the File.stat/2 and File.lstat/2 functions return.

I'm going to copy the metadata from one file to another using File.stat/2 and File.write_stat/3.

#Look at the metadata for "badger.txt"
iex> File.stat("badger.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 48, 31}},
   ctime: {{2019, 5, 17}, {19, 48, 31}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 48, 31}},
   size: 38,
   type: :regular,
   uid: 0
 }}
#Compare that to the metadata in "empty_file.txt"
iex> File.stat("empty_file.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 53, 53}},
   ctime: {{2019, 5, 17}, {19, 53, 53}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 53, 53}},
   size: 0,
   type: :regular,
   uid: 0
 }}
#Retrieve the metadata from "badger.txt"
iex> {:ok, metadata} = File.stat("badger.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 48, 31}},
   ctime: {{2019, 5, 17}, {19, 48, 31}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 48, 31}},
   size: 38,
   type: :regular,
   uid: 0
 }}
#Write that metadata to "empty_file.txt"
iex> File.write_stat("empty_file.txt", metadata)
:ok
#Verify that the "empty_file.txt" metadata was updated
iex> File.stat("empty_file.txt")
{:ok,
 %File.Stat{
   access: :read_write,
   atime: {{2019, 5, 17}, {19, 48, 31}},
   ctime: {{2019, 5, 17}, {19, 48, 31}},
   gid: 0,
   inode: 0,
   links: 1,
   major_device: 3,
   minor_device: 0,
   mode: 33206,
   mtime: {{2019, 5, 17}, {19, 48, 31}},
   size: 0,
   type: :regular,
   uid: 0
 }}

Conclusion

That's it for the File module. Most of the functions in this module mimic Unix command-line tools used to manipulate the file system. So if any of your Elixir code needs to manage files in the file system, it's a good idea to pay close attention.