Lezione 4 di 6 · 25 min di lettura
File e riga di comando
Un programma che ricorda le cose tra un'esecuzione e l'altra, e che riceve istruzioni quando lo lanci. Leggere e scrivere file con simplifile, salvare JSON su disco, e gli argomenti della riga di comando con argv.
Un programma che dimentica tutto
Finora ogni programma è ripartito da zero a ogni gleam run: i dati erano scritti nel codice, e quello che il programma calcolava spariva quando finiva. Anche gli attori del modulo 7 dimenticano tutto quando il programma si spegne.
Per ricordare qualcosa tra un’esecuzione e l’altra, un programma deve scriverlo fuori da sé: in un file. E per fare cose diverse a ogni esecuzione, deve poter ricevere istruzioni da chi lo lancia: gli argomenti della riga di comando. Per entrambe le cose ci sono due pacchetti piccoli e molto usati:
gleam add simplifile argvLeggere e scrivere file
simplifile ha una funzione per ogni operazione sui file. Le tre fondamentali:
| Funzione | Cosa fa |
|---|---|
simplifile.read(from: percorso) | Legge tutto il file come testo: Result(String, FileError). |
simplifile.write(to: percorso, contents: testo) | Scrive il testo nel file, creandolo o sostituendo quello che c’era. |
simplifile.append(to: percorso, contents: testo) | Aggiunge il testo in fondo al file, creandolo se non c’è. |
Tutte restituiscono un Result, perché con i file può sempre andare storto qualcosa: il file non esiste, la cartella non esiste, non hai i permessi, il disco è pieno.
import gleam/io
import gleam/string
import simplifile
pub fn main() -> Nil {
let assert Ok(Nil) = simplifile.write(to: "notes.txt", contents: "buy milk\n")
let assert Ok(Nil) =
simplifile.append(to: "notes.txt", contents: "call Joe\n")
io.println(string.inspect(simplifile.read("notes.txt")))
io.println(string.inspect(simplifile.read("nowhere/notes.txt")))
}Ok("buy milk\ncall Joe\n")
Error(Enoent)Dopo averlo lanciato, nella cartella exercises trovi un file notes.txt con due righe. Il \n alla fine di ogni riga è l’a capo (lezione 1.6): i file di testo sono una lunga stringa, e le righe sono separate da quel carattere.
Il secondo read fallisce: la cartella nowhere non esiste. L’errore Enoent ha un nome strano perché viene dal sistema operativo: è l’abbreviazione di Error: NO ENTry, “nessuna voce con questo nome”. FileError ha una variante per ogni errore del sistema, Eacces (permesso negato), Enospc (disco pieno) e così via, e simplifile.describe_error le traduce in una frase: No such file or directory.
Dettagli nerd Cos'è un percorso? (e la cartella di lavoro)
Un percorso (path) dice dove si trova un file, cartella dopo cartella: /home/ada/learn-gleam/exercises/notes.txt. Un percorso che comincia con / è assoluto: parte dalla radice del disco, e vale da qualsiasi posto (su Windows comincia con una lettera, come C:\).
Un percorso come notes.txt o data/books.json è relativo: parte dalla cartella di lavoro, quella in cui ti trovi quando lanci il programma. Con gleam run è la cartella del progetto, dove sta gleam.toml; per questo notes.txt è finito lì, e non dentro src/. Se lanciassi lo stesso programma da un’altra cartella, cercherebbe e creerebbe i file lì.
Salvare JSON su disco
File e JSON insieme fanno la cosa più utile di tutte: salvare i propri dati in modo strutturato, e rileggerli la volta dopo. Con due fonti di errore diverse, però: il file potrebbe non leggersi (simplifile.FileError), o potrebbe non contenere JSON valido (json.DecodeError). Come nel modulo 5, le riuniamo in un tipo nostro, con result.map_error:
import gleam/dynamic/decode
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/result
import simplifile
pub type Book {
Book(title: String, year: Int)
}
pub type ShelfError {
FileError(simplifile.FileError)
JsonError(json.DecodeError)
}
const path = "books.json"
pub fn main() -> Nil {
let books = [Book("Dune", 1965), Book("Emma", 1815)]
let assert Ok(Nil) = save(books)
case load() {
Ok(books) ->
list.each(books, fn(book) {
io.println(book.title <> " (" <> int.to_string(book.year) <> ")")
})
Error(FileError(error)) ->
io.println("Cannot read the file: " <> simplifile.describe_error(error))
Error(JsonError(_)) -> io.println("The file is not a valid bookshelf")
}
}
fn save(books: List(Book)) -> Result(Nil, ShelfError) {
let text = json.array(books, book_to_json) |> json.to_string
simplifile.write(to: path, contents: text)
|> result.map_error(FileError)
}
fn load() -> Result(List(Book), ShelfError) {
use text <- result.try(simplifile.read(path) |> result.map_error(FileError))
json.parse(text, decode.list(book_decoder()))
|> result.map_error(JsonError)
}
fn book_to_json(book: Book) -> json.Json {
json.object([
#("title", json.string(book.title)),
#("year", json.int(book.year)),
])
}
fn book_decoder() -> decode.Decoder(Book) {
use title <- decode.field("title", decode.string)
use year <- decode.field("year", decode.int)
decode.success(Book(title:, year:))
}Dune (1965)
Emma (1815)E in books.json c’è il JSON compatto della lezione precedente: [{"title":"Dune","year":1965},{"title":"Emma","year":1815}]. Prova ad aprirlo con un editor e a rovinarlo, togliendo una parentesi; poi commenta la riga let assert Ok(Nil) = save(books) e rilancia: il programma risponde The file is not a valid bookshelf, invece di schiantarsi. Nota che FileError e JsonError sono varianti con dati, usate come funzioni da result.map_error (lezione 4.3).
Quiz
Il programma chiama simplifile.write(to: "log.txt", contents: "B") su un file che contiene A. Cosa contiene il file dopo?
Gli argomenti della riga di comando
Quando lanci un programma dal terminale, puoi scrivere delle parole dopo il suo nome: sono gli argomenti. Con gleam run, gli argomenti per il tuo programma vanno dopo un doppio trattino, --, che separa quelli per gleam da quelli per il programma:
gleam run -m greet -- AdaIl pacchetto argv li legge: argv.load().arguments è una List(String), con una stringa per ogni argomento. E una lista si smonta con il pattern matching, come nel modulo 3:
import argv
import gleam/io
pub fn main() -> Nil {
case argv.load().arguments {
[] -> io.println("Hello, stranger!")
[name] -> io.println("Hello, " <> name <> "!")
_ -> io.println("Usage: gleam run -m greet -- [name]")
}
}Hello, stranger!Questa è l’uscita senza argomenti. Con un argomento:
gleam run -m greet -- AdaHello, Ada!e con due, gleam run -m greet -- Ada Joe, il programma risponde con le istruzioni per l’uso. Gli argomenti sono separati dagli spazi; per passare una frase intera come un solo argomento, la metti tra virgolette: gleam run -m greet -- "Ada Lovelace".
Il -- si può omettere se nessun argomento comincia con un trattino. Ma se scrivi gleam run -m greet --verbose, gleam pensa che --verbose sia per lui, e risponde unexpected argument '--verbose' found, suggerendo proprio di usare -- --verbose. Mettere sempre il -- evita la sorpresa.
Dettagli nerd Da dove arrivano gli argomenti?
Quando il terminale avvia un programma, il sistema operativo gli consegna la lista delle parole scritte dopo il nome: è un meccanismo antico quanto Unix, e in C quella lista si chiama argv (argument vector), da cui il nome del pacchetto. Il terminale la prepara prima di lanciare il programma: divide la riga negli spazi, e toglie le virgolette, che servono solo a lui per capire dove finisce un argomento. Per questo il tuo programma riceve Ada Lovelace senza virgolette.
Esercizio · sul tuo computer
Il diario
Crea src/diary.gleam, un diario da riga di comando che salva le voci in diary.txt, una per riga:
gleam run -m diary -- add "testo"aggiunge una voce in fondo al file e stampaSaved;gleam run -m diary -- readstampa le voci numerate (1. ...,2. ...), oppureThe diary is emptyse il file non esiste ancora;- con argomenti diversi, stampa le istruzioni per l’uso.
Usa una costante per il nome del file. Prova la sequenza:
gleam run -m diary -- add "Learned about files"
gleam run -m diary -- add "Wrote JSON"
gleam run -m diary -- reade incolla qui l’output dell’ultimo comando.
Mostra una soluzione (prima prova da solo!)
import argv
import gleam/int
import gleam/io
import gleam/list
import gleam/string
import simplifile
const diary = "diary.txt"
pub fn main() -> Nil {
case argv.load().arguments {
["add", entry] -> add(entry)
["read"] -> read()
_ -> io.println("Usage: gleam run -m diary -- add <entry> | read")
}
}
fn add(entry: String) -> Nil {
case simplifile.append(to: diary, contents: entry <> "\n") {
Ok(Nil) -> io.println("Saved")
Error(error) -> io.println("Error: " <> simplifile.describe_error(error))
}
}
fn read() -> Nil {
case simplifile.read(diary) {
Ok(text) ->
text
|> string.trim_end
|> string.split("\n")
|> list.index_map(fn(line, index) {
int.to_string(index + 1) <> ". " <> line
})
|> list.each(io.println)
Error(simplifile.Enoent) -> io.println("The diary is empty")
Error(error) -> io.println("Error: " <> simplifile.describe_error(error))
}
}string.trim_end toglie l’ultimo a capo prima di dividere il testo in righe: senza, string.split produrrebbe una riga vuota in fondo, e il diario stamperebbe un 3. senza niente. Il pattern Error(simplifile.Enoent) distingue il caso “file non ancora creato”, che non è un vero errore, da tutti gli altri.
Ricapitolando
simplifile(gleam add simplifile):read,write(sostituisce),append(aggiunge in fondo); tutte restituiscono unResultcon unFileError.Enoentvuol dire “non esiste”;simplifile.describe_errorspiega ogni errore a parole.- I percorsi relativi partono dalla cartella di lavoro: con
gleam run, la cartella del progetto. - File e JSON insieme salvano dati strutturati;
result.map_errorriunisce errori di tipo diverso in un tipo tuo. argv(gleam add argv):argv.load().argumentsè la lista degli argomenti, da smontare concase.- Gli argomenti per il programma vanno dopo
--:gleam run -m greet -- Ada.
Nella prossima lezione, prima della sfida finale: come si organizza un programma che non sta più in un file solo.