ZIG - Lavorare con i files
NB: capitolo parziale
Questa sezione della gestione riguarda la gestione dei files. Si tratta di nozioni di base che dovrebbero essere sufficienti per iniziare a lavorare in maniera abbastanza produttiva, diciamo così. Andremo avanti in maniera abbastanza schematica, ritengo sia l'approccio più semplice.
Iniziamo con la ricerca dell'esistenza di un file.
Non c'è,
purtroppo, un
"exists" che farebbe comodo. Il metodo idiomatico è cercare il file e
intercettare il messaggio di file non trovato.
Iniziamo quindi ottenendo
un handle alla directory
const file = std.Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch
(err)
la funzione cwd restituisce un elemento Dir, non fa I/O direttamente ma
restituisce il nostro handle, cioè quello che ci serve, cwd in
sostanza vuol dire current working directory.
openFile è un metodo su Dir che usa
l'handle già impacchettato da cwd() come
"base" per una risoluzione di path relativa, il tutto passando attraverso
l'astrazione Io. Si nota la chiamata di openFile alla interfaccia
io che contiene tutta una serie di
operazioni di input/output.
Copia e incolla dal sito ufficiale:
A cross-platform interface that abstracts all I/O operations and concurrency. It includes:
- file system
- networking
- processes
- time and sleeping
- randomness
- async, await, concurrent, and cancel
- concurrent queues
- wait groups and select
- mutexes, futexes, events, and conditions
- memory mapped files This interface allows programmers to write optimal, reusable code while participating in these operations.
Abbiamo poi il path passato per via parametrica e infine .{ }
ovvero una struct anonima. Quest'ultima è estremamente importante in quanto compilatore, sapendo dalla firma che il tipo
atteso è OpenFileOptions, trovando la struct vuota interpreta come: "crea un valore di tipo
OpenFileOptions usando tutti i valori di default dei suoi campi". Vedremo
tra breve un esempio di specifica dei parametri.
Non è semplicissimo ma questa verbosità ci assicura un pieno controllo su
tutti i parametri e le fasi delle operazioni.
L'ultima parte
catch
|err| switch (err)
riguarda eventuali errori, come file non trovato o
altri. Degli errori gestiti in questo modo parliamo in altro paragrafo.
Spiegata la parte più pesante, vediamo il codice completo che cerca l'esistenza di un file:
Esempio 25.1
const std = @import("std");
fn fileExists(io: std.Io, path: []const u8) !bool {
const file = std.Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch (err) {
error.FileNotFound => return false,
else => return err,
};
file.close(io);
return true;
}
pub fn main(init: std.process.Init) !void {
const exists = try fileExists(init.io, "aaa.txt");
std.debug.print("Esiste: {}\n", .{exists});
}
error.FileNotFound => return false,
else => return err,
queste due istruzioni riguardano ovviamente la gestione di eventuali errori.
file.close(io)
è l'istruzione necessaria per chiudere e rilasciare il file con il parametro io che permette di sapere tramite cosa eseguire l'operazione di chiusura. Potete provare ad eseguire il programma fornendo nella stessa directory in cui si trova l'eseguibile un file "aaa.txt" (o potete cambiare il nome a vostro piacimento, si capisce, oppure indicando il nome di un file che non esiste in modo da verificare il diverso comportamento.
Creiamo - attenzione, sovrascrive - un file e scriviamo dentro di esso.
Di seguito il codice di base
Esempio 25.2
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const cwd = std.Io.Dir.cwd();
const file = try cwd.createFile(io, "test.txt", .{});
defer file.close(io);
var write_buf: [1024]u8 = undefined;
var file_writer = file.writer(io, &write_buf);
const writer = &file_writer.interface;
try writer.writeAll("ciao\n");
try writer.writeAll("mondo\n");
try writer.flush(); // fondamentale: senza flush i dati restano nel buffer
}
quindi, init è una istanza di std.process.Init
che contiene un insieme di API preinizializzate. Tra queste troviamo
io che abbiamo descritto in precedenza. In
buona sostanzaconst io = init.io;
contiene tutto ciò.
Stesso
discorso per const cwd = std.Io.Dir.cwd
Alla riga successiva abbiamo:
const file = try cwd.createFile(io, "test.txt", .{});
ovvero una operazione che può fallire, come si vede da quel
try, e che tra i parametri il nome completo
del file e una struct che essendo vuota, come sempre significa, lo
ripetiamo:
"Crea una struct del tipo che la funzione si aspetta, ma non
specificare alcun campo a mano: usa tutti i valori predefiniti presi dalla
definizione della struct ".
Definiamo quindi un writer che alla fine è solo un buffer di scrittura e lo puntiamo tramite
var file_writer = file.writer(io, &write_buf);
che dobbiamo definire come variabile perchè la sua crescita è progressiva
const writer = &file_writer.interface;
Qui prendiamo l'indirizzo (&) del campo interface dentro file_writer. Il risultato è un *std.Io.Writer cioè un puntatore all'interfaccia generica di scrittura.
L'ultima istruzione ovvero:
try writer.writeAll("ciao\n");
scrive nel buffer che poi dovrà essere svuotato trazmite flush() che troviamo come ultima istruzione.
Da notare che se volessimo aprire un file evitando però di riscriverlo se questo esiste bisognerebbe intervenire sulla struct interna cambiando l'istruzione di creazione del file da:
const file = try cwd.createFile(io, "test.txt", .{});
a:
const file = try cwd.createFile(io, "test.txt", .{ .exclusive = true
});
Presento infine un programma che copia il testo di un file e lo copia in un altro contando le righe, come riassunto di quanto visto finora. In particolare leggeremo da un file "aaa.txt", che deve esistere altrimenti il programma va in crash, in "bbb.txt" (non sono molto originale con i nomi...).
Esempio 25.3
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const allocator = init.gpa;
const cwd = std.Io.Dir.cwd();
// Leggiamo aaa.txt interamente in memoria
const contents = try cwd.readFileAlloc(io, "aaa.txt", allocator, .limited(10 * 1024 * 1024)); // max 10 MB
defer allocator.free(contents);
// Contiamo le righe (splitScalar su '\n')
var line_count: usize = 0;
var it = std.mem.splitScalar(u8, contents, '\n');
while (it.next()) |_| : (line_count += 1) {}
// Se il file finisce con \n, l'ultimo "pezzo" è vuoto: non lo conto come riga reale
if (contents.len > 0 and contents[contents.len - 1] == '\n') {
line_count -= 1;
}
// Scriviamo il contenuto in bbb.txt
try cwd.writeFile(io, .{
.sub_path = "bbb.txt",
.data = contents,
});
std.debug.print("Copiate {d} righe da aaa.txt a bbb.txt\n", .{line_count});
}
Prendetelo come esempio avente valenza generale.