Vala III: práce se soubory

Jak jsem slíbil, tak činím. Pro zatím se ale nebudeme pouštět do žádných větších akcí ;) a tak se podíváme jak pracovat se soubory. Jako i jindy se lze k jednomu výsledku dopracovat několika způsoby, některé si tedy představíme.

22.9.2013 00:00 | Ondřej Tůma | přečteno 10132×

stdin, stdout, stderr

Ve vale, se k základní práci se soubory používá třída FileStream. Vytváří se statickou metodou open s parametry cesty k souboru a módem otevření, nebo fopen s parametry file descriptor a mód. Tedy celkem standarní přístup. A jak by asi C / C++ programátoři očekávali, třída obsahuje standardní metody jako printf, putc, getc, eof, flush, seek, read, write a další.

V minulém článku jsem zmínil základní virtuální soubory, běžně dostupné na všech posixových systémech stdin, stdout a stderr. Tyto soubory jsou dostupné jako proměnné balíku glib a proto je není nutné nijak inicializovat. A protože je GLib namespace implicitně dostupný, není třeba jej explicitně uvádět.

public static int main (string[] args){

    char x = (char) stdin.getc();

    stderr.printf("%s: Chybový text\n", args[0]);   // Netřeba uvádět GLib namespace
    GLib.stdout.printf("Zadali jste znak %c\n", x); // nic ale nezkazíme ;)

    return 0;
}
Soubor stdinouterr.vala

Správnost si můžete ověřit například přesměrování jednoho z výstupů do /dev/null, nebo do souboru:

~$ valac stdinouterr.vala
~$ echo E | ./stdinouterr
~$ echo K | ./stdinouterr 2>/dev/null

Třída FileStream obsahuje běžné metody, které znáte z jiných jazyků, a které Vám umožní číst a zapisovat znaky, řádky nebo bloky dat. Pro čtení řádků se vysloveně hodí metoda read_line, a pro zápis printf s koncem řádku na konci.

public static int main (string[] args) {
	// Otevře soubor filestream.vala pro čtení
	FileStream stream = FileStream.open ("filestream.vala", "r");
	assert (stream != null);    // neúspěch ukončí aplikaci

        string ? line;              // obsah proměnné line může být roven null

        // line je přirazena hodnota z read_line a ta je následně testována
        // na null
        while ((line = stream.read_line()) != null) {
            stdout.puts(line);      // do stdout vloží přečtený řádek
            stdout.putc('\n');      // do stdout vloží znak nového řádku
        }

	return 0;
}
soubor filestream.vala

V uvedeném příkladu používám některé již probrané možnosti, ale raději je znovu vysvětlím. Nejprve ale od začátku. Otevřeme soubor filestream.vala pro čtení, název i s módem otvírání je předat v parametrech statické metodě FileStream.open.

Příkazem assert otestujeme správné vytvoření třídy, ale vždyť stream nemůže být null?! Ve skutečnosti může. Deklarace bez otazníku způsobí, že na některých místech volá Vala assert za nás, ne však všude. V našem příkladu tento assert test být nemusí, protože volání metody read_line interně tento test stejně provede. Díky tomu by ani nemusel být string line deklarován z otazníkem. Programátorovi i překladači by ale mělo být zřejmé, co v proměnné může být. Budeme chovat slušně a ono se nám to dříve, nebo později rozhodně vyplatí.

Konstrukce while by neměla být ničím zajímavá, prostě načte díky metodě read_line řádek, a ten pak testuje na správné přiřazení. Pokud vše proběhne jak má, na stdout se vloží přečtený řádek a po něm i znak nového řádku, protože read_line vrací znak bez něj.

Než se posuneme dál, zastavím se ale u metody read_line. Možná byste místo složitého přiřazování a následného porovnávání přečteného řádku, chtěli použít metodu eof. Je třeba si ale uvědomit, že metoda read_line ve skutečnosti čte znak po znaku (což můžete dělat také), a pokud narazí na znak '\n' nebo na EOF, ukončí čtení. A pokud do té doby nic nepřečetla (ani konec řádku), vrátí null, nikoli prázdný string. Díky tomu, že většina editorů na konci posledního řádku, vkládá znak odřádkování a pak až znak EOF, může se snadno stát, že EOF přečtený nebude, a read_line vrátí null, takže kontrola metodou eof nebude fungovat.

A co uzavření souboru ? FileStream je třída, a ta má svůj konstruktor i destruktor, ten sice není vidět, ale volá se v době, kdy Vala zjistí, že už objekt nepotřebujete. Na místě tohoto destruktoru uvidíte ve vygenerovaném C souboru právě uzavření otevřeného souboru.

Za domácí úkol si zkuste tento primitivní příklad rozšířit o délku přečteného řádku, nebo o jeho číslo.

Metody read a write

Dřív nebo později ovšem budete chtít číst, nebo zapisovat binární data. Logickým krokem, by pro Vás mohlo být použití metod read a write. Jejich použití je ale malinko zrádné a je třeba u nich přemýšlet v čistém C.

Metody očekávají pole čísel o velikosti 8mi bitů (uint8). Pokud máte data uložena v poli čísel, vlastně nemusíte druhý parametr zadávat, implicitní hodnota je 1. Pokud ale přetypováváte pole nějakých struktur, počet položek v poli se nemění, ale velikost struktury ano. Musíte pak uvést druhý parametr, ve kterém říkáte jak velká ta datová struktura je.

Problém může také nastat, pokud poslední čtení nepřepíše celý buffer. Prakticky se to dá obejít tak, že zpracovávat budete tu část bufferu, do které byla data skutečně načtena. Oba problémy si ukážeme v následujících příkladech:

public static int main (string[] args) {
    // Otevře soubor filestream.vala pro čtení v binárním režimu
    FileStream stream = FileStream.open ("filestream2.vala", "rb");

    uint8 buf[20];                          // pole 20ti byte
    size_t size;

    while ((size = stream.read(buf)) > 0) { // přečte jeden blok 20ti čísel
        stdout.write(buf[0:size]);          // do stdout vloží výsek z bloku
    }

    return 0;
}
soubor filestream2.vala

Otevření souboru, resp. vytvoření objektu třídy FileStream už známe, jen je doplněn mód o binární příznak. Tento příznak vlastně na posixových systémech nic nedělá, ale je dobré ho uvádět pro lepší orientaci. Následuje deklarace pole 20ti čísel o velikosti jeden byte. Toto pole by bylo v čistém C naprosto neinicializované, Vala ovšem vygeneruje kód, ve kterém inicializuje první hodnotu a tím donutí překladač nastavit všechny položky pole na hodnotu 0.

Konstrukce while je velmi podobná předchozímu příkladu. Zajímavé je však volání metody read. Metoda, jak už sem napsal, přečte pole čísel o velikosti definovaného pole, v našem případě tedy 20.

Následuje zápis pole do stdout. Výsek z pole (buf[0:size]) bude známý zejména pythonistům. Tento zápis vytvoří pole o nové velikosti, která je definovaná právě rozsahem v hranatých závorkách. Pokud by metoda write dostala celé pole, zapsala by i část nepřepsaných dat přečtených v předchozím cyklu, což není žádaný stav. Nakonec ještě zmíním, že v případě metody printf by toto samozřejmě nefungovalo, protože metoda printf očekává string, tedy pole znaků, které je ukončeno hodnotou 0 (což nemusí být pravda) a nově vytvořené pole buf[0:size], by skoro určitě ukazovalo na původní data v paměti.

public struct Data {        // jednoduchá datová struktura
    char l;
    char h;

    public Data(char x) {   // i struktura může mít konstruktor
        l = x.tolower();
        h = x.toupper();
    }
}

public static int main (string[] args) {
    // pole datových struktor
    Data [] mem = {Data('a'), Data('h'), Data('o'), Data('j')};

    stdout.write((uint8 []) mem, sizeof(Data));     // zapis pole struktur
                        // sizeof vrací velikost struktury Data v paměti
    stdout.putc('\n');
    return 0;
}
soubor memstream.vala

V první části programu definujeme veřejnou strukturu Data. O strukturách jsem psal v úvodní části tohoto seriálu. Jde vlastně o velmi jednoduchou podobu objektu. Struktura má dvě vlastnosti l a h. V konstruktoru této struktury tyto vlastnosti nastavujeme.

a Ah H o Oj J
Obraz paměti, jak v ní je uloženo pole mem

Magie začíná. V druhé části, tedy ve funkci main, vytváříme pole těchto struktur. Pole má 4 prvky a každý prvek zabírá v paměti 2 znaky (byty). Přetypováním tohoto pole na pole bytů získáme zase pole o čtyřech prvcích. První byte ukazuje na první znak první struktury, druhý na druhy znak první struktury, třetí na první znak druhé struktury a čtvrtý na druhý znak druhé struktury. Ale protože potřebujeme zapsat všechny prvky v poli mem, musíme metodě write předat jako druhý parametr velikost jedné datové struktury. Metoda write pak bude správně číst data z paměti od adresy proměnné mem po adresu mem + mem.length * sizeof(Data).

File

Než takový soubor otevřeme, je vhodné ho například otestovat, zda existuje. Někdy navíc potřebujeme se souborem dělat další „systémové“ věci. Kopírovat ho, vytvářet, měnit jeho parametry, získat jeho cestu atd. K tomu slouží třída File z balíčku gio. V minulém díle jsem naznačil cosi o dalších vala knihovnách – balíčcích. S těmi se budeme postupně setkávat, dnes použijeme první z nich. Něž k tomu však dojde, malinko odbočím.

Dalo by se říci, že balíček je synonymum pro knihovnu. Je třeba ale mít na paměti, že tomu tak vždy nemusí být. Autor dotyčného vapi (vala api) balíčku, může za jeden takový balíček schovat podporu několika souvisejících knihoven, nebo obráceně, jednu knihovnu rozdělit do několika balíčků. Standardní vala balíček odpovídá balíčku definovaného přes pkg-config. Pkg-config je nástroj, který vrátí potřebné informace pro kompilování a linkování s danou knihovnou. Tento nástroj využívá celá rodina GNOME knihoven, od té nejspodnější – GLib, až po tu nejvyšší - GNOME. A možná nejen díky tomu, že podporuje i závislosti, nebo kontrolu verzí, existuje podpora pkg-configu napříč celým open-source světem. Jedna knihovna, má často několik samostatných balíčků, a tak do budoucna budu vždy uvádět pouze balíček, a případně i celou knihovnu, ze které balíček je.

Nyní už si můžeme ukázat malý kód na testování přítomnosti souboru.

public static int main (string[] args) {
    // vytvoření třídy jedním z konstruktorů:
    File file = File.new_for_path ("ctime.txt");

    if (file.query_exists ()) {                 // kontrola zda soubor existuje
	stdout.printf ("Soubor '%s' existuje\n", file.get_path());
    } else {                                    // nebo nikoli
	stdout.printf ("Soubor 'ctime.txt' neexistuje\n");
    }

    return 0;
}
soubor vala_exist.vala

Uvedený příklad kompilujeme, je však nutné uvést balíček gio, jež obsahuje třídu File:

~$ valac file_exist.vala --pkg=gio-2.0

Co se vlastně v programu děje? Nejprve vytvoříme objekt file, podobně jako v případě FileStream, jednou ze statických metod. Třída File má několik statických metod pro vytváření svých instancí, my vytváříme objekt z názvu souboru včetně cesty, konkrétně z relativní cesty. No a proč neuvádím namespace ? Protože balíček gio patří do rodiny GLib knihoven, a tak používá GLib namespace, který jak už jsme si řekli, je standardně integrován do všech vala programů.

Zbytek kódu testuje, zda soubor existuje, a pokud existuje, je vypsána celá jeho cesta. To znamená, že relativní cesta je doplněna o aktuální adresář, ve kterém se program pouští, a tedy ve kterém se soubor ctime.txt nachází.

Třída File obsahuje mnoho zajímavých metod, některé dokonce rovnou otvírají soubory pro čtení nebo zápis. Jsou to například metody append_to, crate, read nebo jejich asynchroní modifikace. Tyto metody vrací objekty, které reprezentují opravdové soubory. Nejde však o FileStream ale o InputStream, OutputStream nebo IOStream, který je zastřešuje, nikoli však objektově (oba objekty jsou vlastnostmi).

Tyto třídy jsou též součástí gio balíčku, dědí z třídy GLib.Object a tedy dědí i její vlastnosti jako je ref_count nebo signal notify. Všechny zmíněné třídy z gio balíčku již pracují s výjimkami.

public static int main (string[] args) {
    // Vytvoří třídu file, nikoli soubor test.log
    File file = File.new_for_path ("test.log");

    try {                               // v bloku může nastat výjimka
        // až zde se vytvoří soubor test.log, pokud ovšem neexistuje
        FileOutputStream os = file.append_to (FileCreateFlags.NONE);

        // objekt string, má vlastnost data, která je čirou náhodou uint8[]
	os.write ("Další nový řádek\n".data);

        // odchycení výjimky GLib.Error
    } catch (Error e) {                 // odchycení výjimky
	stdout.printf ("Error: %s\n", e.message);
        return 1;
    }

    return 0;
}
soubor append_to_log.vala

Jak je v ukázce uvedeno, vytvoření objektu file rozhodně nevytváří žádný soubor. To je mimo jiné také důvod proč tato třída nemá žádnou metodu close. Zato jeho metoda append, již soubor vytváří, parametrem mu říkáme zda má soubor vytvořit přístupný všem, nebo jen uživateli, pod nímž je program spuštěn. O to jak se to děje na různých systémech se starat nemusíme, na každém to znamená něco jiného. Většina knihoven, které budeme používat a na kterých je Vala, resp, GTK, potažmo GNOME postaveno, obsahuje jistou míru abstrakce, díky níž nemusíte větvit program podle toho, na jakém systému byl spuštěn.

Třída FileOutputStream má mimo jiných metodu write, které se stejně jako v případě FileStream předává pole čísel – bytů. A čirou náhodou objekt string, i když je to objekt spíše abstraktní, obsahuje vlastnost data, která se nám hodí. A ani zde soubor nezavíráme, i když nám to třída FileOutputStream dovoluje. I zde platí že ztráta reference na objekt vyvolá destruktor, a ten se o vše postará sám. Ve vygenerovaném C souboru již žádné volání close nenajdete, to proto, že jde o potomky třídy GLib.Object a u nich se i v čistém C volá „destruktor“ funkcí g_object_unref. To mimo jiné znamená, že tuto funkcionalitu mají třídy z gio balíčku i ve své čisté C podobě.

K výjimkám se ještě dostaneme, proto jejich popis zatím vynechám a kód řádně otestujeme. Zkompilujeme, dvakrát spustíme, změníme práva výstupního souboru, a ověříme, že opravdu došlo k chybě.

~$ valac append_to_log.vala –pkg=gio-2.0
~$ ./append_to_log
~$ ./append_to_log
~$ chmod u-w test.log
~$ ./append_to_log

Gio a ještě dál.

Balíček gio toho však obsahuje daleko víc, než jen pár tříd pro práci se soubory. Umí pracovat i s disky, sockety, sítí, nebo třeba s uživatelským nastavením aplikace. Pokud budete potřebovat nějaký balíček, pomocí nějž byste otvírali soubory ze sítě, gio je ten, který hledáte. Vedle gio balíčku existuje ještě gio-unix, ten obsahuje třídy, jenž využijete jen na unixových systémech (unix socket nebo disková přípojná místa).

Od příštího dílu již začneme probírat grafické rozhraní GTK+. Je toho celkem dost, tak doufám že se máte na co těšit. Na úplný závěr ještě zmíním, že gio je součástí GLib knihovny (je distribuován pohromadě), tedy zatím používáme to, co je pro Valu nejzákladnější vybavení.

knihovna GLib
Vala dokumentace ke GLib.FileStream
knihovna GIO
Vala dokumentace gio balíčku
pkg-config
Příručka programu pkg-configl

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1995