Trabajando con el sistema de archivos en Elixir

Trabajar con el sistema de archivos en Elixir realmente no difiere de hacerlo utilizando otros lenguajes de programación populares. Hay tres módulos para resolver esta tarea: IO, Expediente, y Camino. Proporcionan funciones para abrir, crear, modificar, leer y destruir archivos, expandir rutas, etc. Sin embargo, existen algunos errores interesantes que debe tener en cuenta..

En este artículo hablaremos sobre cómo trabajar con el sistema de archivos en Elixir mientras echamos un vistazo a algunos ejemplos de código..

El módulo de camino

El módulo Ruta, como su nombre indica, se utiliza para trabajar con las rutas del sistema de archivos. Las funciones de este módulo siempre devuelven cadenas codificadas en UTF-8.

Por ejemplo, puede expandir una ruta y luego generar una ruta absoluta fácilmente:

Path.expand ('./ text.txt') |> Path.absname # => "f: /elixir/text.txt"

Tenga en cuenta, por cierto, que en Windows, las barras invertidas se reemplazan con barras inclinadas automáticamente. El camino resultante se puede pasar a las funciones del Expediente módulo, por ejemplo:

Path.expand ('./ text.txt') |> Path.absname |> File.write ("new content!", [: Write]) # =>: ok

Aquí estamos construyendo una ruta completa al archivo y luego le escribimos algunos contenidos..

En definitiva, trabajar con el Camino El módulo es simple y la mayoría de sus funciones no interactúan con el sistema de archivos. Veremos algunos casos de uso para este módulo más adelante en el artículo..

IO y módulos de archivos

IO, como su nombre lo indica, es el módulo para trabajar con entrada y salida. Por ejemplo, proporciona funciones tales como pone y inspeccionar. IO tiene un concepto de dispositivos, que pueden ser identificadores de proceso (PID) o átomos. Por ejemplo, hay : stdio y : stderr Dispositivos genéricos (que en realidad son accesos directos). Los dispositivos en Elixir mantienen su posición, por lo que las operaciones de lectura o escritura posteriores comienzan desde el lugar donde anteriormente se accedió al dispositivo.

El módulo de archivos, a su vez, nos permite acceder a los archivos como dispositivos IO. Los archivos se abren en modo binario por defecto; sin embargo, puede pasar : utf8 como una opción. También cuando un nombre de archivo se especifica como una lista de caracteres ('some_name.txt'), siempre se trata como UTF-8.

Ahora veamos algunos ejemplos del uso de los módulos mencionados anteriormente..

Abrir y leer archivos con IO

La tarea más común es, por supuesto, abrir y leer archivos. Para abrir un archivo, se puede usar una función llamada open / 2. Acepta una ruta al archivo y una lista opcional de modos. Por ejemplo, tratemos de abrir un archivo para leer y escribir:

: ok, file = File.open ("test.txt", [: read,: write]) file |> IO.inspect # => #PID<0.72.0>

Luego puede leer este archivo usando la función read / 2 de la IO módulo también:

: ok, file = File.open ("test.txt", [: read,: write]) IO.read (file,: line) |> IO.inspect # => "test" IO.read (file ,: línea) |> IO.inspect # =>: eof

Aquí estamos leyendo el archivo línea por línea. Nota la : eof átomo que significa "fin del documento".

También puedes pasar :todos en lugar de :línea para leer todo el archivo a la vez:

: ok, file = File.open ("test.txt", [: read,: write]) IO.read (file,: all) |> IO.inspect # => "test" IO.read (file ,: todos) |> IO.inspect # => "" 

En este caso, : eof no se devolverá, en su lugar, obtenemos una cadena vacía. ¿Por qué? Bueno, porque, como dijimos anteriormente, los dispositivos mantienen su posición y comenzamos a leer desde el lugar al que se accedió anteriormente..

También hay una función abierta / 3, que acepta una función como tercer argumento. Una vez que la función pasada ha terminado su trabajo, el archivo se cierra automáticamente:

File.open "test.txt", [: read], fn (file) -> IO.read (file,: all) |> IO.inspect end

Lectura de archivos con módulo de archivo

En la sección anterior he mostrado cómo usar IO.read para leer archivos, pero parece que el Expediente El módulo realmente tiene una función con el mismo nombre:

File.read "test.txt" # => : ok, "test"

Esta función devuelve una tupla que contiene el resultado de la operación y un objeto de datos binarios. En este ejemplo contiene "prueba", que es el contenido del archivo..

Si la operación no tuvo éxito, entonces la tupla contendrá una :error átomo y la razón del error:

Archivo.read ("non_existent.txt") # => : error,: enoent

aquí, : enoent Significa que el archivo no existe. Hay algunas otras razones como : eacces (no tiene permisos).

La tupla devuelta se puede usar en la coincidencia de patrones para manejar diferentes resultados:

case File.read ("test.txt") do : ok, body -> IO.puts (body) : error, reason -> IO.puts ("Hubo un error: # reason") fin

En este ejemplo, imprimimos el contenido del archivo o mostramos un motivo de error.

Otra función para leer archivos se llama leer! / 1. Si has venido del mundo Ruby, probablemente has adivinado lo que hace. Básicamente, esta función abre un archivo y devuelve su contenido en forma de cadena (¡no tuple!):

File.read! ("Test.txt") # => "test"

Sin embargo, si algo sale mal y el archivo no se puede leer, se genera un error:

File.read! ("Non_existent.txt") # => (File.Error) no pudo leer el archivo "non_existent.txt": no existe tal archivo o directorio

Entonces, para estar seguro, puede, por ejemplo, emplear la función existe? / 1 para verificar si realmente existe un archivo: 

defmodule Ejemplo do def read_file (file) do si File.exists? (file) do File.read! (file) |> IO.inspect end end end end Example.read_file ("non_existent.txt")

Genial, ahora sabemos como leer archivos. Sin embargo, hay mucho más que podemos hacer, así que pasemos a la siguiente sección!

Escribir archivos

Para escribir algo en un archivo, use la función write / 3. Acepta una ruta a un archivo, los contenidos y una lista opcional de modos. Si el archivo no existe, se creará automáticamente. Sin embargo, si existe, todos sus contenidos se sobrescribirán de forma predeterminada. Para evitar que esto suceda, configure el :adjuntar modo:

File.write ("new.txt", "update!", [: Append]) |> IO.inspect # =>: ok

En este caso, los contenidos se adjuntarán al archivo y :De acuerdo será devuelto como resultado. Si algo sale mal, obtendrás una tupla : error, razón, al igual que con el leer función.

Además, hay una escritura! función que hace prácticamente lo mismo, pero genera una excepción si no se pueden escribir los contenidos. Por ejemplo, podemos escribir un programa de Elixir que crea un programa de Ruby que, a su vez, imprime "hola":

File.write! ("Test.rb", "pone \" hola! \ "")

Archivos de transmisión

Los archivos pueden ser bastante grandes, y al usar el leer Función que carga todos los contenidos en la memoria. La buena noticia es que los archivos se pueden transmitir con bastante facilidad:

Archivo.open! ("Test.txt") |> IO.stream (: line) |> Enum.each (& IO.inspect / 1)

En este ejemplo, abrimos un archivo, lo transmitimos línea por línea e inspeccionamos cada línea. El resultado se verá así:

"prueba \ n" "línea 2 \ n" "línea 3 \ n" "alguna otra línea ... \ n"

Tenga en cuenta que los nuevos símbolos de línea no se eliminan automáticamente, por lo que es posible que desee deshacerse de ellos utilizando la función String.replace / 4.

Es un poco tedioso transmitir un archivo línea por línea como se muestra en el ejemplo anterior. En cambio, puede confiar en la función stream! / 3, que acepta una ruta al archivo y dos argumentos opcionales: una lista de modos y un valor que explica cómo se debe leer un archivo (el valor predeterminado es :línea):

File.stream! ("Test.txt") |> Stream.map (& (String.replace (& 1, "\ n", ""))) |> Enum.each (& IO.inspect / 1)

En este fragmento de código, transmitimos un archivo mientras eliminamos caracteres de nueva línea y luego imprimimos cada línea.. File.stream! es más lento que File.read, pero no tenemos que esperar hasta que todas las líneas estén disponibles, podemos comenzar a procesar los contenidos de inmediato. Esto es especialmente útil cuando necesita leer un archivo desde una ubicación remota.

Echemos un vistazo a un ejemplo un poco más complejo. Me gustaría transmitir un archivo con mi script Elixir, eliminar caracteres de nueva línea y mostrar cada línea con un número de línea al lado:

File.stream! ("Test.exs") |> Stream.map (& (String.replace (& 1, "\ n", ""))) |> Stream.with_index |> Enum.each (fn (contents , line_num) -> IO.puts "# line_num + 1 # contents" fin)

Stream.with_index / 2 acepta un enumerable y devuelve una colección de tuplas, donde cada tupla contiene un valor y su índice. A continuación, simplemente iteramos sobre esta colección e imprimimos el número de línea y la línea en sí. Como resultado, verá el mismo código con los números de línea:

1 File.stream! ("Test.exs") |> 2 Stream.map (& (String.replace (& 1, "\ n", ""))) |> 3 Stream.with_index |> 4 Enum.each ( fn (contents, line_num) -> 5 IO.puts "# line_num + 1 # contents" 6 final)

Mover y eliminar archivos

Ahora veamos también brevemente cómo manipular archivos, específicamente, moverlos y eliminarlos. Las funciones que nos interesan son rename / 2 y rm / 1. No lo aburriré describiendo todos los argumentos que aceptan, ya que puede leer la documentación usted mismo y no hay absolutamente nada complejo en ellos. En su lugar, echemos un vistazo a algunos ejemplos..

Primero, me gustaría codificar una función que tome todos los archivos del directorio actual según una condición y luego los mueva a otro directorio. La función debería llamarse así:

Copycat.transfer_to "texts", fn (archivo) -> Path.extname (archivo) == ".txt" fin

Así que, aquí quiero agarrar todo .TXT archivos y moverlos a la textos directorio. ¿Cómo podemos resolver esta tarea? Bueno, primero, definamos un módulo y una función privada para preparar un directorio de destino:

Defmodule Copycat do def transfer_to (dir, fun) do prepare_dir! dir end defp prepare_dir! (dir) do a menos que File.exists? (dir) do File.mkdir! (dir) end end end end end

mkdir !, como ya has adivinado, intenta crear un directorio y devuelve un error si esta operación falla.

A continuación, necesitamos capturar todos los archivos del directorio actual. Esto se puede hacer usando el ls! función, que devuelve una lista de nombres de archivos:

Archivo.ls!

Por último, debemos filtrar la lista resultante en función de la función proporcionada y cambiar el nombre de cada archivo, lo que efectivamente significa moverlo a otro directorio. Aquí está la versión final del programa:

Defmodule Copycat do def transfer_to (dir, fun) do prepare_dir! (dir) File.ls! |> Stream.filter (& (fun. (& 1))) |> Enum.each (& (File.rename (& 1, "# dir / # & 1"))) end defp prepare_dir! (Dir) do a menos que File.exists? (dir) do File.mkdir! (dir) end end end end

Ahora veamos el rm en acción codificando una función similar que eliminará todos los archivos en función de una condición. La función se llamará de la siguiente manera:

Copycat.remove_if fn (file) -> Path.extname (file) == ".csv" end

Aquí está la solución correspondiente:

defmodule Copycat do def remove_if (fun) do File.ls! |> Stream.filter (& (fun. (& 1))) |> Enum.each (& File.rm! / 1) end end

rm! / 1 generará un error si el archivo no se puede eliminar. Como siempre, tiene una contraparte rm / 1 que devolverá una tupla con el motivo del error si algo sale mal..

Usted puede observar que el remove_if y transferir a Las funciones son muy similares. Entonces, ¿por qué no eliminamos la duplicación de código como un ejercicio? Agregaré otra función privada que toma todos los archivos, los filtra según la condición proporcionada y luego les aplica una operación:

defp filter_and_process_files (condición, operación) haga File.ls! |> Stream.filter (& (condición. (& 1))) |> Enum.each (& (operación. (& 1))) final

Ahora simplemente utiliza esta función:

defmódulo Copycat do def transfer_to (dir, fun) do prepare_dir! (dir) filter_and_process_files (fun, fn (file) -> File.rename (file, "# dir / # file") end) end def remove_if ( fun) do filter_and_process_files (fun, fn (file) -> File.rm! (file) end) end # ... end

Soluciones de terceros

La comunidad de Elixir está creciendo y están surgiendo nuevas bibliotecas sofisticadas que resuelven diversas tareas. El repositorio Awesome Elixir GitHub enumera algunas soluciones populares y, por supuesto, hay una sección con bibliotecas para trabajar con archivos y directorios. Hay implementaciones para cargar archivos, monitorear, desinfectar nombres de archivos, y más.

Por ejemplo, hay una solución interesante llamada Librex para convertir sus documentos con la ayuda de LibreOffice. Para verlo en acción, puedes crear un nuevo proyecto:

$ mix nuevo convertidor

Luego agregue una nueva dependencia al archivo mix.exs:

 defp deps do [: librex, "~> 1.0"] end

Después de eso, ejecute:

$ mix do deps.get, deps.compile

A continuación, puede incluir la biblioteca y realizar conversiones:

defmodule Converter no importa Librex def convert_and_remove (dir) do convierte "some_path / file.odt", "other_path / 1.pdf" end end

Para que esto funcione, el ejecutable de LibreOffice (soffice.exe) debe estar presente en el CAMINO. De lo contrario, deberá proporcionar una ruta a este archivo como tercer argumento:

defmodule Converter no importa Librex def convert_and_remove (dir) do convierte "some_path / file.odt", "other_path / 1.pdf", "path / soffice" end end

Conclusión

¡Eso es todo por hoy! En este artículo, hemos visto la IO, Expediente y Camino módulos en acción y discutió algunas funciones útiles como abierto, leer, escribir, y otros. 

Hay muchas otras funciones disponibles para su uso, así que asegúrese de consultar la documentación de Elixir. Además, hay un tutorial introductorio en el sitio web oficial del idioma que también puede ser útil.

Espero que haya disfrutado este artículo y que ahora se sienta un poco más seguro de trabajar con el sistema de archivos en Elixir. Gracias por estar conmigo, y hasta la próxima.!