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:

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 sostanza
const 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.