|
In questo capitolo ci prendiamo una piccola pausa dall'illustrare concetti e strutture dati di questo linguaggio e passiamo ad un aspetto eminentemente pratico ovvero vediamo schematicamente come lavorare sui files di testo in Rust. 1) Verificare se un file esiste: usiamo il modulo std::path che è estremamente potente e lavora in comunione con il sistema operativo sottostante, sia esso Windows, Linux o Mac. Esso contiene due tipi Path e PathBuf idealmente simili ma con caratteristiche diverse. Path 1) Lavora per riferimento - non possiede i dati 2) è immutabile 3) usato per operazioni di sola lettura sui files PathBuf 1) Possiede i dati 2) è mutabile 3) Usato per compiti più ampi su percorsi e files. Il seguente rapido esempio mostra la differenza tra i due:
In questo caso notiamo usando Path_Buf possiamo effettuare delle modifiche, precisamente alla riga 6, cosa non possibile usando Path. Ad esempio per gestire l'input dell'utente in sede di modifica del path è necessario usare PathBuf. E' possibile costruire un percorso "a pezzi" come indica anche un semplice esempio direttamente dalla documentazione ufficiale: let path: PathBuf = ["c:\\", "windows", "system32.dll"].iter().collect(); Passiamo ad altri esempi pratici e vediamo quindi un semplice essempio che verifica l'esistenza di un file e in questo caso, essendo operazione di sola lettura, ci appoggeremo su Path:
Si tratta di un esempio veramente di base se volete una cosa più completa, a titolo di esempio che può venirvi utile ai fini pratici, ecco qua un programma che vi consente di inserire il percorso del file direttamente dallo standard input:
Della gestione dell'input parliamo in apposito paragrafo. Un'altra differenza tra Path e PathBuf è la loro creazione.Vediamo il seguente codice: let pathbuf = PathBuf::from("aaa.txt"); let path = Path::from("aaa.txt"); questo non compilerebbe a causa della seconda riga. Il compilatore ci dice che:the trait bound `Path: From<_>` is not satisfied ovvero non è possibile usare From da qualsiasi fonte per creare un Path (mentre per PathBuf va bene). Bisogna ricorrere a new, come nel primo esempio. Creare un nuovo file Un sistema molto comodo per creare un nuovo file è usare il modulo std::fs che fornisce molti utili metodi per lavorare sui file. In praticolare, per lo scopo di questa sezione, abbiamo il metodo create(nomefile), abbastanza esplicativo. Esso restituisce un Result in cui i due possibili risultato sono il file oppure l'errore che ne ha impedito la creazione.
use std::fs::File;
fn main() {
match File::create("aaa.txt") {
Ok(file) => println!("File creato correttamente"),
Err(error) => println!("Errore durante la
creazione del file: {}", error),
}
}
Il codice direi che si commenta da solo. Abbiamo creato un file di aaa.txt senza nulla dentro. Questo programma non si fa scrupolo di sovrariscrivere un eventuale file con lo stesso nome elimininandone quindi l'eventuale contenuto preesistente, per cui, così com'è, è anche piuttosto pericoloso. Una via alternativa e più sicura anche se un po' più complessa è la seguente:
La riga 1 ci presenta una comoda variante di use che non abbiamo ancora visto con quel "self, ErrorKind" che ci permette di usare l'enumerazione ErrorKind senza io:: davanti. Si tratta, come detto, di una semplice comodità d'uso. Di suo ErrorKind è una enumerazione molto ricca con circa 40 varianti che prendono in esame un grande numero di casi relativi alla gestione non solo dei file ma del I/O in generale. Merita un'occhiata approfondita. La riga 2 abbiamo OpenOptions una struct il cui percorso è std::fs::OpenOptions che viene bene descritta nella documentazione ufficiale "Options and flags which can be used to configure how a file is opened." Esso ci mette a disposizione un set completo di metodi per la gestione dei files che ricapitoliamo qui sotto: use std::fs::OpenOptions; let file = OpenOptions::new() .read(true) .write(true) .create(true) .open("foo.txt"); come intuibile con questa serie di metodi possiamo creare (se già non esistono), leggere, scrivere e aprire file. Rispetto al semplice Create ovviamente ci portiamo dietro qualcosa di più pesante. Aprire e scrivere su un nuovo file e scrivere in coda a file esistente esempio base, direi autoesplicativo: use std::fs::File; use std::io::Write; fn main() { // Crea un nuovo file di nome "aaa.txt" let mut file = File::create("aaa.txt").expect("Impossibile creare il file"); // Scrive la riga nel file file.write_all(b"sono il file aaa.txt").expect("Impossibile scrivere nel file"); } Il programma seguente invece verifica se il file esiste già e nel caso non sovrascrive ma ne esce un messaggio di errore, anche in questo caso direi che è tutto chiaro sulla base delle conoscenze acquisite: use std::fs::OpenOptions; use std::io::Write; use std::path::Path; fn main() { let path = Path::new("aaa.txt"); if path.exists() { println!("File già esistente"); } else { let mut file = OpenOptions::new() .create(true) .write(true) .open(path) .expect("Impossibile creare il file"); file.write_all(b"sono il file aaa.txt").expect("Impossibile scrivere nel file"); } } Infine vogliamo un programma che scriva in coda al nostro "aaa.txt", qui c'è qualche piccola novità:
La riga 5 ci presenta il metodo append che, qualora sia true, abilita la scrittura in coda al file. La riga 8 invece espone file.write_all che è utilizzata per scrivere un buffer di byte completo in un file o in un altro tipo di stream. Si noti, sempre alla riga 8, quella "b" che precede la stringa da scrivere, che indica che si quella sche segue è una stringa di byte, in questa caso ' necessario tale precisazione in quanto write_all lavora su stringhe di byte. NB: il programma precedente va in panico se il file su cui dovete appendere il testo non esiste. E' necessario quindi verificarne prima l'esistenza Leggere il contenuto di un file e memorizzarlo in un Vec Questa operazione può essere utile per manipolare gli elementi interni ad un file.
I commenti indicano la strada da seguire, ovvero apriamo il file in lettura File::open che ci dà un errore se il file non esiste, salvo che poi il programma crasha in panico. la riga 9 introduce BufReader che viene utilizzata per il buffering delle operazioni di lettura su un lettore (reader) sottostante. La procedura di bufferizzazione migliora molto le operazioni di questo tipo dal punto di vista prestazionale. La riga 11 crea un vettore di vettori di caratteri, ciascuno dei quali, questi ultimo, conterrà una riga del file. La lettura scrittura inizia evidentemente alla riga 13. BufReader restituisce un Result quindi la riga 14 serve a gestire la condizione di eventuale errore. La riga 16 crea una riga del vettore lines. Le righe 19 e 20 stampano per verifica il vettore. Memorizzare il contenuto di un file in una stringa Possiamo usare fs::read_to_string per compiere l'operazione descritta. È un modo conveniente per leggere file di testo senza dover gestire manualmente i buffer di lettura. Il seguente programma memorizza il contenuto di un file in una stringa e lo stampa a video. Direi che non presenta difficoltà:
use std::fs;
fn main() {
let file_path = "aaa.txt";
match fs::read_to_string(file_path) {
Ok(contents) => println!("Contenuto del
file:\n{}", contents),
Err(e) => println!("Errore durante la lettura del
file: {}", e),
}
}
Copiare un file di testo in un altro Vediamo subito l'esempio che ci presenta il modo più veloce e semplice:
usiamo il metodo copy che restituisce il solito result. Cancellare un file Anche qui esempio rapido simile al precedente:
Usiamo remove_file e anche qui abbiamo un Result. Ricavare data di creazione e dimensione di un file. Presento un esempio utile per le finalità illustrate
Come vedete sono esposti i metadati dei un file, il nostro solito "aaa.txt". Vi invito a consultare la documentazione ufficiale per ulteriori dettagli su tali metadati. Di seguito alcuni esempi: Dimensione del file: Utilizzando il metodo len, puoi ottenere la dimensione del file in byte. let file_size = metadata.len(); Tipo di file: Utilizzando il metodo file_type, puoi ottenere il tipo di file (file regolare, directory, link simbolico, ecc.). let file_type = metadata.file_type(); Permessi: Utilizzando il metodo permissions, puoi ottenere i permessi del file. let permissions = metadata.permissions(); Ultima modifica: Utilizzando il metodo modified, puoi ottenere l’ultima data di modifica del file. let modified_time = metadata.modified().expect("Impossibile ottenere la data di modifica"); Data di creazione: Utilizzando il metodo created, puoi ottenere la data di creazione del file (se supportato dal sistema operativo). let created_time = metadata.created().expect("Impossibile ottenere la data di creazione"); Ultimo accesso: Utilizzando il metodo accessed, puoi ottenere l’ultima data di accesso al file. let accessed_time = metadata.accessed().expect("Impossibile ottenere la data di accesso"); È una directory: Utilizzando il metodo is_dir, puoi verificare se il file è una directory. let is_directory = metadata.is_dir(); È un file regolare: Utilizzando il metodo is_file, puoi verificare se il file è un file regolare. let is_file = metadata.is_file(); È un link simbolico: Utilizzando il metodo is_symlink, puoi verificare se il file è un link simbolico. let is_symlink = metadata.is_symlink(); La gestione dei file in Rust può essere trattata ed impostata anche in altri modi, la quantità di possibilità che offre questo linguaggio sono davvero tante. In questo paragrafo, come detto all'inizio dal taglio più pratico che teorico, comunque dovreste aver trovato tutto quanto serve per lavorare da subito in maniera completa. Nel prossimo capitolo tuttavia esploreremo altri modi di interagire con i files per cui, per avere una panoramica più completa, vi consiglio di leggere anche quello. |