IO y el sistema de ficheros
Introducción
En esta clase verás una breve introducción a los mecanismos de entrada y salida, así como también tareas relacionadas con el sistema de ficheros por medio de los módulos IO
, File
y Path
.
Módulo IO
El módulo IO
es el mecanismo principal en Elixir para leer y escribir a la entrada o salida estándar, conocida como :stdio
; estándar error, conocido como :stderr
, ficheros, y otros dispositivos de entrada y salida. Veamos algunos ejemplos:
iex> IO.puts("hello world")
hello world
:ok
iex> IO.gets("yes or no? ")
yes or no? yes
"yes\n"
Por omisión, las funciones del módulo IO
leen desde la entrada estándar y escriben a la salida estándar. Podemos cambiar dicho comportamiento al pasar, por ejemplo, :stderr
como un argumento, para escribir al dispositivo estándar para errores.
iex> IO.puts(:stderr, "hello world")
hello world
:ok
El módulo File
El módulo File
contiene funciones que permiten abrir ficheros como dispositivos IO o de entrada-salida. Por omisión, los ficheros son abiertos en modo binario, lo cual requiere que los desarrolladores usen las funciones IO.binread/2
y IO.binwrite/2
del módulo IO
:
iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.47.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("hello")
{:ok, "world"}
Un fichero también puede ser abierto con codificación :utf8
, lo cual le indica al módulo File
que interprete los bytes que lee desde el fichero como bytes codificados en UTF8.
Aparte de las funciones para abrir, leer y escribir ficheros, el módulo File
tiene muchas funciones para trabajar con el sistema de ficheros. Dichas funciones fueron llamadas siguiendo como referencia los nombres en sistemas UNIX o equivalentes. Por ejemplo, File.rm/1
puede ser usada para remover ficheros, File.mkdir/1
para crear directorios. File.mkdir_p/1
para crear directorios de manera recursiva de ser necesario. Incluso encuentras funciones como File.cp_r/2
y File.rm_rf/1
para copiar o remover ficheros de manera recursiva.
También notarás que hay funciones en el módulo File
que tienen dos variantes: una variante “regular” y otra variante que finaliza con un signo de exclamación o bang (!
). Por ejemplo, cuando lees el fichero "hello"
en el ejemplo anterior, se usa File.read/1
. Alternativamente, podrías usar File.read!/1
:
iex> File.read("hello")
{:ok, "world"}
iex> File.read!("hello")
"world"
iex> File.read("unknown")
{:error, :enoent}
iex> File.read!("unknown")
** (File.Error) could not read file "unknown": no such file or directory
Nota que la versión con !
retorna el contenido del fichero en vez de una tupla, y si algo va mal, la función genera una excepción.
La versión sin !
es preferida cuando deseas manejar diferentes resultados por medio de pattern matching:
case File.read(file) do
{:ok, body} -> # do something with the `body`
{:error, reason} -> # handle the error caused by `reason`
end
Sin embargo, si esperas que el fichero esté allí, la variación con el bang es más útil porque emite un mensaje de error descriptivo. Evita escribir:
{:ok, body} = File.read(file)
Dado que, en el caso de error, File.read/1
retornará {:error, reason}
y la coincidencia de patrones fallará. Si bien obtendras el resultado esperado, una excepción, pero el mensaje de error será acerca de una coincidencia de patrones que falló, lo cual no es preciso en este caso acerca del error que en realidad ocurrió.
Por lo tanto, si no deseas manejar la salida de los errores, prefiere el uso de File.read!/1
.
El módulo Path
La mayoría de las funciones en el módulo File
esperan rutas como argumentos. Regularmente, dichas rutas serán binarios o strings. El módulo Path
provee algunas facilidades para trabajar con dichas rutas:
iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"
Te recomiendo usar las funciones del módulo Path cuando manejes rutas en vez de manipular las cadenas directamente dado que dicho módulo maneja diferentes sistemas operativos de manera transparente. Finalmente, ten en cuenta que Elixir automáticamente convertirá slashes (/
) en backslashes (\
) en Windows cuando realiza operaciones sobre ficheros.
Con esto, hemos cubierto los principales módulos provistos por Elixir para interactuar con dispositivos de entrada-salida y el sistema de ficheros.
Processes
You may have noticed that File.open/2
returns a tuple like {:ok, pid}
:
iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.47.0>}
That happens because the IO
module actually works with processes (see chapter 11). Given a file is a process, when you write to a file that has been closed, you are actually sending a message to a process which has been terminated:
iex> File.close(file)
:ok
iex> IO.write(file, "is anybody out there")
{:error, :terminated}
Let’s see in more detail what happens when you request IO.write(pid, binary)
. The IO
module sends a message to the process identified by pid
with the desired operation. A small ad-hoc process can help us see it:
iex> pid = spawn(fn ->
...> receive do: (msg -> IO.inspect msg)
...> end)
#PID<0.57.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #Reference<0.0.8.91>,
{:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated
After IO.write/2
, we can see the request sent by the IO
module printed out (a four-elements tuple). Soon after that, we see that it fails since the IO
module expected some kind of result, which we did not supply.
By modeling IO devices with processes, the Erlang VM allows I/O messages to be routed between different nodes running Distributed Erlang or even exchange files to perform read/write operations across nodes. Neat!
Esto finaliza nuestra gira por dispositivos de entrada-salida y funcionalidades relacionadas. Aprendimos sobre tres módulos IO
, File
, y Path
.Así como también cómo la máquina virtual usa procesos de entrada-salida subyacentes.