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
- A tisztítási példa minden állítása teljesül, és a számozás a megtartott sorok között folytonos.
- 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.
- 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.