Lavorare con il file system in elisir

Lavorare con il file system in Elixir non è molto diverso dal farlo utilizzando altri linguaggi di programmazione popolari. Ci sono tre moduli per risolvere questo compito: IO, File, e Sentiero. Forniscono funzioni per aprire, creare, modificare, leggere e distruggere file, espandere percorsi, ecc. Esistono, tuttavia, alcuni trucchi interessanti di cui dovresti essere a conoscenza.

In questo articolo parleremo di lavorare con il file system in Elixir, dando uno sguardo ad alcuni esempi di codice.

Il modulo del percorso

Il modulo Path, come suggerisce il nome, viene utilizzato per lavorare con i percorsi del file system. Le funzioni di questo modulo restituiscono sempre stringhe con codifica UTF-8.

Ad esempio, puoi espandere un percorso e quindi generare facilmente un percorso assoluto:

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

Nota, a proposito, che in Windows, i backslash vengono sostituiti automaticamente con le barre in avanti. Il percorso risultante può essere passato alle funzioni di File modulo, ad esempio:

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

Qui stiamo costruendo un percorso completo per il file e quindi scrivendone alcuni contenuti.

Tutto sommato, lavorando con Sentiero il modulo è semplice e la maggior parte delle sue funzioni non interagisce con il file system. Vedremo alcuni casi d'uso per questo modulo più avanti nell'articolo.

Moduli I / O e File

IO, come suggerisce il nome, è il modulo per lavorare con input e output. Ad esempio, fornisce funzioni come mette e ispezionare. IO ha un concetto di dispositivi, che possono essere identificatori di processo (PID) o atomi. Ad esempio, ci sono : stdio e : stderr dispositivi generici (che sono in realtà scorciatoie). I dispositivi in ​​elisir mantengono la loro posizione, quindi le successive operazioni di lettura o scrittura iniziano dal luogo in cui il dispositivo è stato precedentemente consultato.

Il modulo File, a sua volta, ci consente di accedere ai file come dispositivi IO. I file vengono aperti in modalità binaria per impostazione predefinita; tuttavia, potresti passare : utf8 come opzione Anche quando un nome file viene specificato come elenco di caratteri ('Some_name.txt'), viene sempre considerato UTF-8.

Ora vediamo alcuni esempi di utilizzo dei moduli sopra menzionati.

Apertura e lettura dei file con IO

Il compito più comune è, naturalmente, aprire e leggere i file. Per aprire un file, è possibile utilizzare una funzione chiamata open / 2. Accetta un percorso per il file e un elenco opzionale di modalità. Ad esempio, proviamo ad aprire un file per leggere e scrivere:

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

È quindi possibile leggere questo file utilizzando la funzione di lettura / 2 dal file IO modulo pure:

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

Qui stiamo leggendo il file riga per riga. Notare la : eof atomo che significa "fine del file".

Puoi anche passare :tutti invece di :linea leggere l'intero file in una sola volta:

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

In questo caso, : eof non verrà restituito, ma otteniamo una stringa vuota. Perché? Bene, perché, come abbiamo detto prima, i dispositivi mantengono la loro posizione, e iniziamo a leggere dal posto precedentemente accessibile.

C'è anche una funzione open / 3, che accetta una funzione come terzo argomento. Dopo che la funzione passata ha terminato il suo lavoro, il file viene chiuso automaticamente:

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

Lettura dei file con il modulo file

Nella sezione precedente ho mostrato come usare IO.read per leggere i file, ma sembra che il File il modulo ha effettivamente una funzione con lo stesso nome:

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

Questa funzione restituisce una tupla contenente il risultato dell'operazione e un oggetto dati binario. In questo esempio contiene "test", che è il contenuto del file.

Se l'operazione non ha avuto successo, allora la tupla conterrà un :errore atom e il motivo dell'errore:

File.read ("non_existent.txt") # => : errore,: enoent

Qui, : ENOENT significa che il file non esiste. Ci sono altri motivi come : EACCES (non ha permessi).

La tupla restituita può essere utilizzata nella corrispondenza del modello per gestire diversi risultati:

caso File.read ("test.txt") do : ok, corpo -> IO.puts (corpo) : errore, ragione -> IO.puts ("C'è stato un errore: # motivo") fine

In questo esempio, stampiamo il contenuto del file o visualizziamo un motivo di errore.

Un'altra funzione per leggere i file è chiamata read! / 1. Se vieni dal mondo Ruby, probabilmente hai indovinato cosa fa. Fondamentalmente, questa funzione apre un file e ne restituisce il contenuto sotto forma di stringa (non tupla!):

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

Tuttavia, se qualcosa va storto e il file non può essere letto, viene invece generato un errore:

File.read! ("Non_existent.txt") # => (File.Error) non può leggere il file "non_existent.txt": nessun file o directory

Quindi, per essere al sicuro, è possibile, ad esempio, utilizzare la funzione? / 1 esistente per verificare se un file esiste effettivamente: 

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

Bene, ora sappiamo come leggere i file. Tuttavia, possiamo fare molto di più, quindi passiamo alla prossima sezione!

Scrivere su file

Per scrivere qualcosa su un file, utilizzare la funzione write / 3. Accetta un percorso per un file, i contenuti e un elenco opzionale di modalità. Se il file non esiste, verrà creato automaticamente. Se, tuttavia, esiste, tutto il suo contenuto verrà sovrascritto per impostazione predefinita. Per evitare che ciò accada, impostare il :aggiungere modalità:

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

In questo caso, il contenuto verrà aggiunto al file e :ok sarà restituito come risultato. Se qualcosa va storto, otterrai una tupla : errore, motivo, proprio come con il leggere funzione.

Inoltre, c'è una scrittura! funzione che fa più o meno la stessa cosa, ma solleva un'eccezione se non è possibile scrivere il contenuto. Ad esempio, possiamo scrivere un programma Elixir che crea un programma Ruby che, a sua volta, stampa "ciao!":

File.write! ("Test.rb", "puts \" ciao! \ "")

File in streaming

I file possono infatti essere piuttosto grandi e quando si usa il leggere funzione caricate tutti i contenuti nella memoria. La buona notizia è che i file possono essere trasmessi in streaming abbastanza facilmente:

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

In questo esempio, apriamo un file, lo streaming linea per linea e ispezioniamo ogni riga. Il risultato sarà simile a questo:

"test \ n" "riga 2 \ n" "riga 3 \ n" "qualche altra linea ... \ n"

Nota che i nuovi simboli di linea non vengono rimossi automaticamente, quindi potresti volerli eliminare usando la funzione String.replace / 4.

È un po 'noioso eseguire lo streaming di un file riga per riga, come mostrato nell'esempio precedente. Invece, puoi fare affidamento sulla funzione stream! / 3, che accetta un percorso per il file e due argomenti opzionali: un elenco di modalità e un valore che spiega come deve essere letto un file (il valore predefinito è :linea):

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

In questo pezzo di codice stiamo facendo lo streaming di un file mentre rimuoviamo i caratteri di nuova riga e quindi stampiamo ogni riga. File.stream! è più lento di File.read, ma non abbiamo bisogno di aspettare fino a quando tutte le linee sono disponibili: possiamo iniziare subito a elaborare i contenuti. Ciò è particolarmente utile quando è necessario leggere un file da una posizione remota.

Diamo un'occhiata a un esempio leggermente più complesso. Mi piacerebbe eseguire lo streaming di un file con il mio script Elixir, rimuovere i caratteri di nuova riga e visualizzare ogni riga con un numero di riga accanto ad esso:

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

Stream.with_index / 2 accetta un enumerable e restituisce una raccolta di tuple, in cui ogni tupla contiene un valore e il suo indice. Successivamente, eseguiamo semplicemente l'iterazione su questa raccolta e stampiamo il numero di riga e la linea stessa. Di conseguenza, vedrai lo stesso codice con i numeri di riga:

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 fine)

Spostamento e rimozione dei file

Ora esaminiamo anche brevemente come manipolare i file, in particolare spostarli e rimuoverli. Le funzioni a cui siamo interessati sono rename / 2 e rm / 1. Non ti annoierò descrivendo tutti gli argomenti che accettano dato che puoi leggere tu stesso la documentazione, e non c'è assolutamente nulla di complesso in essi. Invece, diamo un'occhiata ad alcuni esempi.

Innanzitutto, vorrei codificare una funzione che prende tutti i file dalla directory corrente in base a una condizione e quindi li sposta in un'altra directory. La funzione dovrebbe essere chiamata così:

Copycat.transfer_to "texts", fn (file) -> Path.extname (file) == ".txt" fine

Quindi, qui voglio afferrare tutto .testo file e spostarli sul testi directory. Come possiamo risolvere questo compito? Bene, in primo luogo, definiamo un modulo e una funzione privata per preparare una directory di destinazione:

defmodule Copycat do def transfer_to (dir, fun) fai prepare_dir! dir end defp prepare_dir! (dir) do meno che File.exists? (dir) do File.mkdir! (dir) end end end

mkdir !, come hai già indovinato, prova a creare una directory e restituisce un errore se questa operazione fallisce.

Successivamente, abbiamo bisogno di prendere tutti i file dalla directory corrente. Questo può essere fatto usando ls! funzione, che restituisce un elenco di nomi di file:

File.ls!

Infine, dobbiamo filtrare l'elenco risultante in base alla funzione fornita e rinominare ogni file, il che significa in effetti spostarlo in un'altra directory. Ecco la versione finale del programma:

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

Ora vediamo il rm in azione codificando una funzione simile che rimuoverà tutti i file in base a una condizione. La funzione verrà chiamata nel seguente modo:

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

Ecco la soluzione corrispondente:

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

rm! / 1 genera un errore se il file non può essere rimosso. Come sempre, ha una controparte rm / 1 che restituirà una tupla con il motivo dell'errore se qualcosa va storto.

Si può notare che il remove_if e trasferire a le funzioni sono molto simili. Quindi, perché non rimuoviamo la duplicazione del codice come esercizio? Aggiungerò un'altra funzione privata che prende tutti i file, li filtra in base alle condizioni fornite e quindi applica loro un'operazione:

defp filter_and_process_files (condizione, operazione) do File.ls! |> Stream.filter (& (condition. (& 1))) |> Enum.each (& (operation. (& 1))) end

Ora usa semplicemente questa funzione:

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

Soluzioni di terze parti

La comunità di elisir sta crescendo e stanno emergendo nuove biblioteche che risolvono vari compiti. Il repository GitHub di Awesome Elixir elenca alcune soluzioni popolari e, naturalmente, c'è una sezione con le librerie per lavorare con file e directory. Esistono implementazioni per il caricamento di file, il monitoraggio, la sanitizzazione dei nomi dei file e altro ancora.

Ad esempio, c'è una soluzione interessante chiamata Librex per convertire i tuoi documenti con l'aiuto di LibreOffice. Per vederlo in azione, puoi creare un nuovo progetto:

$ mixare un nuovo convertitore

Quindi aggiungi una nuova dipendenza al file mix.exs:

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

Successivamente, esegui:

$ mix do deps.get, deps.compile

Successivamente, puoi includere la libreria ed eseguire conversioni:

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

Affinché questo funzioni, l'eseguibile di LibreOffice (soffice.exe) deve essere presente nel SENTIERO. Altrimenti, dovrai fornire un percorso a questo file come terzo argomento:

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

Conclusione

È tutto per oggi! In questo articolo, abbiamo visto il IO, File e Sentiero moduli in azione e discusso alcune funzioni utili come Aperto, leggere, Scrivi, e altri. 

Ci sono molte altre funzioni disponibili per l'uso, quindi assicurati di consultare la documentazione di Elixir. Inoltre, c'è un tutorial introduttivo sul sito web ufficiale della lingua che può tornare utile.

Spero che questo articolo ti sia piaciuto e ora ti senti un po 'più sicuro di lavorare con il file system in Elixir. Grazie per essere stato con me, e fino alla prossima volta!