Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 36
Lavorare sui file

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:

  Esempio 36.1
1
2
3
4
5
6
7
8
use std::path::{Path, PathBuf};
fn main() {
    let path = Path::new("/some/path");
    println!("Il percorso è: {:?}", path);
    let mut path_buf = PathBuf::from("/some/path");
    path_buf.push("file.txt");
    println!("Il percorso modificato è: {:?}", path_buf);
}

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:

  Esempio 36.2
1
2
3
4
5
6
7
8
9
use std::path::Path;
fn main() {
    let path = Path::new("percorso/al/file.txt");
    if path.exists() {
        println!("Il file esiste.");
    } else {
        println!("Il file non esiste.");
    }
}

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:

  Esempio 36.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io;
use std::path::Path;
fn main() {
    // Crea un nuovo buffer per l'input dell'utente
    let mut file_name = String::new();
    // Chiede all'utente di inserire il nome del file
    println!("Inserisci il nome del file:");
    // Legge l'input dell'utente
    io::stdin()
        .read_line(&mut file_name)
        .expect("Errore nella lettura dell'input");
    // Rimuove eventuali spazi bianchi e caratteri di nuova linea
    let file_name = file_name.trim();
    // Crea un oggetto Path con il nome del file inserito dall'utente
    let path = Path::new(file_name);
    // Verifica se il file esiste
    if path.exists() {
        println!("Il file esiste.");
    } else {
        println!("Il file non esiste.");
    }
}

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:

  Esempio 36.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::io::{self, ErrorKind};
use std::fs::OpenOptions;
fn main() {
    let file = match OpenOptions::new()
        .create_new(true) // Imposta su 'true' per creare un nuovo file
        .write(true)      // Aggiungi l'accesso in scrittura
        .open("aaa.txt") {
            Ok(file) => file,
            Err(error) => {
                if error.kind() == ErrorKind::AlreadyExists {
                    println!("File già esistente: aaa.txt");
                } else {
                    println!("Errore durante la creazione del file: {}", error);
                }
                return;
            }
        };
    println!("File creato correttamente");
}

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à:

  Esempio 36.5
1
2
3
4
5
6
7
8
9
use std::fs::OpenOptions;
use std::io::Write;
fn main() {
    let mut file = OpenOptions::new()
        .append(true)
        .open("aaa.txt")
        .expect("Impossibile aprire il file");
    file.write_all(b"\nSono una riga aggiuntiva\n").expect("Impossibile scrivere nel file");
}

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.

  Esempio 36.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
fn main() {
// Apre il file in lettura
    let path = Path::new("aaa.txt");
    let file = File::open(&path).expect("Impossibile aprire il file");
// Crea un buffer per leggere il file riga per riga
    let reader = io::BufReader::new(file);
// Vettore per memorizzare le righe, ogni riga è un vettore di caratteri
    let mut lines : Vec<Vec<char>> = Vec::new();
// Leggi il file riga per riga
    for line in reader.lines() {
        let line = line.expect("Impossibile leggere la riga");
// Converte la riga in un vettore di caratteri e aggiunge al vettore delle righe
        lines.push(line.chars().collect());
    }
// Stampa il contenuto del vettore per verifica
    for (i, line) in lines.iter().enumerate() {
        println!("Riga {}: {:?}", i + 1, line);
    }
}

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:

  Esempio 36.7
1
2
3
4
5
6
7
8
9
use std::fs;
fn main() {
    let sorgente = "aaa.txt";
    let destinazione = "bbb.txt";
    match fs::copy(sorgente, destinazione) {
        Ok(bytes) => println!("File copiato con successo: {} byte copiati", bytes),
        Err(e) => println!("Errore durante la copia del file: {}", e),
    }
}

usiamo il metodo copy che restituisce il solito result.

Cancellare un file
Anche qui esempio rapido simile al precedente:

  Esempio 36.8
1
2
3
4
5
6
7
8
use std::fs;
fn main() {
    let file_to_delete = "aaa.txt";
    match fs::remove_file(file_to_delete) {
        Ok(_) => println!("File cancellato con successo"),
        Err(e) => println!("Errore durante la cancellazione del file: {}", e),
    }
}

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

  Esempio 36.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
    let file_path = "aaa.txt";
    // Ottieni i metadati del file
   let metadata = fs::metadata(file_path).expect("Errore metadata");
    // Ottieni la dimensione del file
    let file_size = metadata.len();
    println!("Dimensione del file: {} byte", file_size);
    // Ottieni la data di creazione del file
    if let Ok(created) = metadata.created() {
        let duration = created.duration_since(UNIX_EPOCH).expect("Impossibile calcolare la durata");
        println!("Data di creazione del file: {} secondi dal 1 gennaio 1970", duration.as_secs());
    } else {
        println!("Impossibile ottenere la data di creazione del file");
    }
}

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.