🦀 rust.hu
gyakorlat30 perc•modul 3/3 · lecke 7/9

Fájlok olvasása és kezelhető hibák

Készíts fájlolvasót, amely megőrzi az operációs rendszer útvonalait, különválasztja az üres bemenetet a hibától, és kiszámítható kimenettel jelez vissza.

Cél és előfeltételek

Ebben a leckében olyan fájlolvasót készítesz, amely a nem üres sorokat számozva írja ki, és várható hibánál használható visszajelzést ad. Előfeltétel az A Rust alapjai útvonal, valamint az Option és Result gyakorlata és az iterátoros adatfeldolgozás. A cél, hogy a sikeres adatutat és a hibás futást ugyanolyan tudatosan tervezd meg.

Egy hiányzó fájl hétköznapi helyzet: elgépelhetik a nevét, megszűnhet egy hálózati csatolás, vagy nem olvasható a tartalma. A programnak nem kell ezek miatt pánikolnia. A függvény visszaadhatja a hibát, a felhasználói felület pedig eldöntheti, hogyan közölje. Ez a szétválasztás később egy grafikus alkalmazásban is használható: ugyanahhoz az olvasóhoz más megjelenítés kapcsolható.

Útvonal, szöveg és hibaterjedés

A fájlnév és a fájl tartalma két külön adat. A std::env::args_os dokumentációja alapján operációs rendszer szerinti argumentumokat kapunk, ezért az útvonalat nem alakítjuk erőltetetten Stringgé. A Path az útvonal kölcsönzött nézete; a példa közvetlenül az OsString argumentumot adja át. A szöveget ugyanakkor UTF-8 formátumban várjuk. Egy megfelelően átadott fájlút tehát még vezethet érvénytelen szöveget tartalmazó fájlhoz.

A read_to_string egyetlen Stringbe tölti a teljes fájlt. A kérdőjel sikertelen olvasásnál korán visszaadja az io::Error értéket. Nem folytatjuk a számítást üres szöveggel, mert a hiányzó bemenet és a valóban üres fájl eltérő állapot. Az unwrap_or_default itt megtévesztő sikert eredményezne. A példa kis gyakorlófájlokra készült; nagy vagy nem megbízható méretű bemenethez külön méretkorlátot és átgondolt olvasási stratégiát kell tervezned.

Mentsd az alábbi teljes programot egy új Cargo-projekt src/main.rs fájljába; a Cargo.toml edition értéke legyen 2024. Függőséget nem kell hozzáadni. A futtatás alakja cargo run -- útvonal; szóközt tartalmazó útvonalat a parancsértelmezőben idézőjelezz. A dupla kötőjel után már a programod kapja az argumentumokat.

use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::process::ExitCode;

fn read_nonempty(path: &Path) -> io::Result<Vec<String>> {
    let text = fs::read_to_string(path)?;
    Ok(text.lines().map(str::trim)
        .filter(|line| !line.is_empty())
        .map(str::to_owned).collect())
}

fn run(path: &Path) -> io::Result<()> {
    let lines = read_nonempty(path)?;
    let mut out = io::stdout().lock();
    for (index, line) in lines.iter().enumerate() {
        writeln!(out, "{}: {line}", index + 1)?;
    }
    Ok(())
}

fn main() -> ExitCode {
    let mut args = env::args_os().skip(1);
    let Some(path) = args.next() else {
        eprintln!("Használat: fajlolvaso <fajl>");
        return ExitCode::from(2);
    };
    if args.next().is_some() {
        eprintln!("Pontosan egy fájlútvonal szükséges.");
        return ExitCode::from(2);
    }
    match run(Path::new(&path)) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("Hiba ({:?}): {error}", error.kind());
            ExitCode::from(1)
        }
    }
}

A read_nonempty kizárólag adatot állít elő, nem ír a konzolra. A trim a sorok széléről eltávolítja a whitespace karaktereket, a filter elhagyja az üres eredményeket. Az új Stringek saját tulajdonúak, így nem hivatkoznak a függvény végén megszűnő text változóra. Az enumerate csak a már megtisztított sorokat számozza: az eredmény sorszámai ezért nem az eredeti fájl sorszámai. Ha naplóhelyeket szeretnél megőrizni, előbb számozz, és utána szűrj.

A run az írást is Resultként kezeli. A write makrók itt nem feltételezik, hogy a stdout mindig elérhető. A main három kimenet közül választ: siker esetén stdout és 0; hibás argumentumszámnál stderr és 2; olvasási vagy kiírási hibánál stderr és 1. Üres fájl sikeres feldolgozásakor nincs kiírt sor. A sikeres stdout gépi feldolgozásra is alkalmas marad, mert nem keverünk hozzá státuszüzeneteket.

Gyakorlófeladatok

1. feladat: szóközök és sorvégek

Írj fájlrendszertől független clean_lines függvényt! Az „alma”, egy üres sor és a szélein szóközös „körte” bemenetből pontosan két elem maradjon. Ellenőrizd a Windows-stílusú sorvéget és a kizárólag whitespace karaktereket tartalmazó bemenetet is. Előbb kézzel írd le a várt eredményt, és csak azután készítsd el az iterátorláncot.

Megoldás
fn clean_lines(text: &str) -> Vec<&str> {
    text.lines().map(str::trim)
        .filter(|line| !line.is_empty()).collect()
}

fn main() {
    assert_eq!(clean_lines("  alma\r\n\r\n körte \n"), vec!["alma", "körte"]);
    assert!(clean_lines(" \n\t\n").is_empty());
    assert_eq!(clean_lines("alma\n"), vec!["alma"]);
}

A kölcsönzött szeletek itt biztonságosak, mert a bemenetet a hívó birtokolja. A záró sorvég nem hoz létre külön üres sort; az üres sorokat ettől függetlenül a filter is elhagyná. Figyeld meg, hogy ebben a változatban egyetlen új szöveget sem kell létrehozni.

2. feladat: használható hibamagyarázat

Készíts magyar magyarázatot három hibafajtához: hiányzó fájl, tiltott hozzáférés, érvénytelen szöveges adat! Minden egyéb hibához legyen általános ág. A döntéshez a hibafajtát vizsgáld, ne az operációs rendszer angol üzenetének részletét keresd, mert annak szövege és nyelve környezetenként eltérhet.

Megoldás
use std::io;

fn error_message(error: &io::Error) -> &'static str {
    match error.kind() {
        io::ErrorKind::NotFound => "A megadott fájl nem található.",
        io::ErrorKind::PermissionDenied => "A fájl olvasásához nincs jogosultság.",
        io::ErrorKind::InvalidData => "A tartalom nem érvényes UTF-8 szöveg.",
        _ => "A fájlművelet nem sikerült.",
    }
}

fn main() {
    assert_eq!(error_message(&io::Error::from(io::ErrorKind::NotFound)),
        "A megadott fájl nem található.");
    assert_eq!(error_message(&io::Error::from(io::ErrorKind::InvalidData)),
        "A tartalom nem érvényes UTF-8 szöveg.");
    assert_eq!(error_message(&io::Error::from(io::ErrorKind::BrokenPipe)),
        "A fájlművelet nem sikerült.");
}

A példában mesterséges hibákkal a besorolási logikát ellenőrizzük. Ez nem bizonyítja a géped jogosultságkezelésének működését. A valódi hibát érdemes a részletes diagnosztikában is megőrizni; az InvalidData üzenet ebben a konkrét szövegolvasási helyzetben utal UTF-8 problémára, általános fájlrendszeres segédfüggvényben ne szűkítsd így a jelentését.

3. feladat: különítsd el az üreset a hibástól

Próbáld ki a teljes programot üres, hiányzó és nem UTF-8 tartalmú fájllal! A próbaadatokat egy frissen létrehozott ideiglenes könyvtárban készítsd el. Ne nevezz át és ne írj felül meglévő dokumentumot. Minden futásnál nézd meg a két kimeneti csatornát és a kilépési állapotot is, mert önmagában a képernyőn látott szöveg kevés bizonyíték.

Megoldás

Az üres fájl eredménye üres stdout, üres stderr és 0. A könyvtáron belüli, létre nem hozott fájl eredménye üres stdout, hiba a stderren és 1. Egyetlen FF hexadecimális bájtot tartalmazó fájlnál szintén 1 az állapot, a hiba fajtája InvalidData. Argumentum nélkül 2 legyen az eredmény. A pontos rendszerüzenetet ne rögzítsd elvárásként; a hibaág és az üres stdout az összehasonlítható viselkedés.

Elfogadási feltételek

  1. A tisztítási példa minden állítása teljesül, és a számozás a megtartott sorok között folytonos.
  2. Hiányzó és érvénytelen UTF-8 bemenetnél a program nem ír sikeres adatot, és 1 állapottal tér vissza; rossz argumentumszámnál 2-vel.
  3. Az útvonal feldolgozása nem használ to_str().unwrap() konverziót, a program pedig egyetlen bemeneti fájlt sem módosít.

Összefoglaló és továbblépés

A kérdőjel a hiba továbbadását rövidíti le; a hibára adott felhasználói választ továbbra is neked kell megtervezned. Az adatszámítás, a fájlolvasás és a konzolos döntés külön felelősség maradt. A következő, tesztelésről szóló leckében ezeket a határokat automatikus ellenőrzésekké alakítjuk.

gyors önellenőrzés

Próbáld ki, mire emlékszel.

Válaszold meg a kérdéseket, majd ellenőrizd a válaszaidat és olvasd el a magyarázatokat. A válaszok az oldal elhagyásakor törlődnek.

1. Mit tesz a read_to_string, ha a létező fájl tartalma nem érvényes UTF-8?
2. Miért használ a példa args_os értékeket a fájlút átvételéhez?
3. Mit jelent a kérdőjel ebben a függvényben?
fn read(path: &std::path::Path) -> std::io::Result<String> {
    let text = std::fs::read_to_string(path)?;
    Ok(text)
}
4. A lecke teljes programját argumentum nélkül indítod. Mi a szerződés szerinti eredmény?

0 / 4 kérdés megválaszolva