Cél és előfeltételek
Az útvonal végén egy használható parancssori eszközzé kapcsolod össze az eddigi elemeket. A program egy UTF-8 szövegfájlból sorokat, szavakat, Unicode-skalárokat és bájtokat számol, majd rendezett gyakoriságlistát ír ki. Előfeltétel a HashMap-gyakorlat, a szövegkezelés, valamint a tesztelési lecke. Most a részfeladatok együttműködése a lényeg: a jó számláló önmagában még nem megbízható CLI.
Mielőtt kódolsz, írd le a kimeneti szerződést. Pontosan egy fájlút az érvényes argumentum; a programnevet nem számítjuk ide. A siker állapota 0, a hiányzó vagy többletargumentumé 2, az olvasási és kiírási hibáé 1. Sikeres futáskor csak az eredmény kerül a stdoutra, hibaüzenet csak a stderrre. Ez megengedi, hogy egy másik program a sikeres eredményt feldolgozza, és külön figyelje a hibát.
Mit tekintünk szónak és karakternek?
A szavak itt whitespace karakterekkel elválasztott darabok. Megtartjuk az írásjeleket és a kisbetű–nagybetű eltérését: az alma, az Alma és az alma! külön kulcs. Ez egyszerű és következetes szabály, de nem általános természetesnyelvi elemzés. A gyakoriságtáblát ezért így nevezd meg a felhasználónak; ne ígérj helyes nyelvészeti szófelismerést olyan bemeneten, amelyre nem tervezted a programot.
A Unicode-skalár nem feltétlenül egy képernyőn látható jel. Egy ékezet előállhat önálló kombináló elemből is; összetett emojik több skalárból épülhetnek fel. A std chars iterátora ilyen skalárokat számol, a len pedig UTF-8 bájtokat. A látható grafémák szegmentálására a standard könyvtár nem kínál általános kész megoldást, ezért a kimenetben szándékosan unicode_scalars a mező neve. Nem normalizáljuk az eltérő Unicode-ábrázolásokat sem.
A sorok meghatározását is rögzítsd: a lines szabályait követjük. Az üres szövegnek nulla sora van; az „x” és az „x” utáni egyetlen sorvég egyaránt egy sort jelent. A CRLF egyetlen sorlezárás, a magában álló CR nem sorhatár ebben az értelmezésben. Ettől a definíciótól egy kizárólag újsorbájtokat számoló eszköz eredménye eltérhet, ezért ne nevezd a programot ellenőrzés nélkül egy másik parancs pontos másolatának.
Teljes program, külön számítással és kiírással
Hozz létre új szovegstat Cargo-projektet edition 2024 beállítással, külső függőségek nélkül. Az alábbi teljes program az src/main.rs tartalma. A fájlút az args_os eredménye marad: nem használunk veszteséges String-konverziót a fájl megnyitása előtt. A tartalom ettől függetlenül UTF-8 kell legyen. A szóközös útvonalat a parancsértelmezőben idézőjelekkel add át.
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::process::ExitCode;
#[derive(Debug, PartialEq, Eq)]
struct Stats {
lines: usize,
words: usize,
unicode_scalars: usize,
bytes: usize,
frequencies: Vec<(String, usize)>,
}
fn analyze(text: &str) -> Stats {
let mut counts = HashMap::<&str, usize>::new();
for word in text.split_whitespace() {
*counts.entry(word).or_default() += 1;
}
let words = counts.values().sum();
let mut frequencies: Vec<_> = counts.into_iter()
.map(|(word, count)| (word.to_owned(), count)).collect();
frequencies.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Stats {
lines: text.lines().count(),
words,
unicode_scalars: text.chars().count(),
bytes: text.len(),
frequencies,
}
}
fn render(stats: &Stats) -> String {
let mut result = format!("lines={}\nwords={}\nunicode_scalars={}\nbytes={}\n",
stats.lines, stats.words, stats.unicode_scalars, stats.bytes);
for (word, count) in &stats.frequencies {
result.push_str(&format!("{}\t{count}\n", word.escape_debug()));
}
result
}
fn run(path: &Path) -> io::Result<()> {
let text = fs::read_to_string(path)?;
io::stdout().lock().write_all(render(&analyze(&text)).as_bytes())
}
fn main() -> ExitCode {
let mut args = env::args_os().skip(1);
let Some(path) = args.next() else {
eprintln!("Használat: szovegstat <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 HashMap iterációs sorrendjére nem építhetünk kimeneti ígéretet. A kulcsokat ezért vektorba gyűjtjük, majd a Rust szöveges összehasonlítása szerint rendezzük. Ez determinisztikus, de nem magyar szótári rendezés: az Alma és alma eltérő helyre kerülhet. Az escape_debug a kulcsban előforduló vezérlőkarakterek látható jelölésével védi a soros megjelenítést; a belső számlálás továbbra is az eredeti szóra történik.
Az analyze csak adatot számít, a render csak szöveget állít össze, a run pedig olvas és ír. Emiatt a kimenet formátuma fájl nélkül is vizsgálható. A write_all hibáját továbbadjuk, tehát egy sikertelen konzolos írást nem minősítünk szándékosan sikernek. Ettől még részleges kiírás előfordulhat I/O-hiba előtt: a folyamat teljes sikerét a 0 állapot jelzi, nem pusztán az első megjelent sor. A példa a teljes fájlt memóriában tartja, nem nagyadat-feldolgozó megoldás.
Gyakorlófeladatok
1. feladat: kézzel ellenőrizhető statisztika
Számold ki kézzel az „A ár”, majd egy külön sorban álló „A” adatait úgy, hogy mindkét sort sorvég zárja! Külön nevezd meg a skalárokat és a bájtokat. Ellenőrizd azt is, hogy a fájl végén lévő egyetlen sorvég nem eredményez plusz sort. A próba célja az eltérő számlálási fogalmak szétválasztása.
Megoldás
fn main() {
let text = "A ár\nA\n";
assert_eq!(text.lines().count(), 2);
assert_eq!(text.split_whitespace().count(), 3);
assert_eq!(text.chars().count(), 7);
assert_eq!(text.len(), 8);
assert_eq!("".lines().count(), 0);
assert_eq!("x\n".lines().count(), 1);
assert_eq!("\n".lines().count(), 1);
}
A helyes eredmény két sor, három szó, hét skalár és nyolc bájt. A különbséget az á kétbájtos UTF-8 ábrázolása adja. A szóköz és a két sorvég a skalár- és bájtszámban szerepel, a szavak számát viszont csak elválasztják.
2. feladat: stabil gyakoriságlista
A „körte alma körte Alma” bemenethez készíts rendezett párokat! A körte kétszer, a másik két kulcs egyszer szerepeljen. A várt listát kézzel add meg. Ne abból indulj ki, hogy a HashMap egy korábbi futásban éppen milyen sorrendet mutatott; az nem a felület szerződése.
Megoldás
use std::collections::HashMap;
fn frequencies(text: &str) -> Vec<(&str, usize)> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut rows: Vec<_> = counts.into_iter().collect();
rows.sort_unstable_by(|a, b| a.0.cmp(b.0));
rows
}
fn main() {
assert_eq!(frequencies("körte alma körte Alma"),
vec![("Alma", 1), ("alma", 1), ("körte", 2)]);
}
Az explicit rendezés biztosítja az összehasonlítható eredményt. A kétféle kezdőbetűt nem vonjuk össze. Ha később kisbetűsítést vezetsz be, módosul a termék viselkedése: ehhez a teszteseteket és a felhasználónak adott magyarázatot is együtt kell változtatnod.
3. feladat: a tényleges CLI ellenőrzése
Fordítsd le a projektet cargo build paranccsal, majd közvetlenül a létrejött szovegstat programot indítsd! Így a Cargo saját fordítási üzenetei nem keverednek a vizsgált stderrbe. Készíts új ideiglenes könyvtárban próbaadatot; a következő Bash-minta macOS és Linux alatt használható. Windows alatt az azonos tartalmú próbaadat és a két kimeneti csatorna ellenőrzése ugyanúgy szükséges, a parancsértelmező szintaxisa eltérhet.
Megoldás
(
set -eu
fixture="$(mktemp -d)"
cleanup() {
rm -f "$fixture/input.txt" "$fixture/expected.txt" \
"$fixture/stdout.txt" "$fixture/stderr.txt"
rmdir "$fixture"
}
trap cleanup EXIT
printf 'A ár\nA\n' > "$fixture/input.txt"
printf 'lines=2\nwords=3\nunicode_scalars=7\nbytes=8\nA\t2\nár\t1\n' \
> "$fixture/expected.txt"
status=0
./target/debug/szovegstat "$fixture/input.txt" \
> "$fixture/stdout.txt" 2> "$fixture/stderr.txt" || status=$?
test "$status" -eq 0
cmp "$fixture/expected.txt" "$fixture/stdout.txt"
test ! -s "$fixture/stderr.txt"
printf 'CLI-ellenőrzés sikeres.\n'
)
A minta a program kilépési állapotát közvetlenül a futásnál menti el; a későbbi ellenőrzések nem írják felül ezt a változót. A cmp bájtról bájtra összeveti a rögzített stdoutot az elvárt eredménnyel, majd külön vizsgáljuk, hogy a stderr üres-e. A trap normál befejezéskor és sikertelen ellenőrzés miatti kilépéskor is takarít; a zárójelbe tett rész saját alfolyamatban fut, így a set beállításai nem módosítják a hívó shellt.
A program rögzített stdoutja pontosan a következő, a stderr üres, a kilépési állapot pedig 0:
lines=2
words=3
unicode_scalars=7
bytes=8
A 2
ár 1
A gyakorisági sorokban a mezők között valódi tabulátor áll. Ellenőrizd külön a nem létező fájlt és az argumentumhiányt is: mindkettő üres stdoutot és nem üres stderrt ad, de az előbbi 1, az utóbbi 2 állapottal zárul. A sikeres futás tesztje önmagában ezeket nem bizonyítja.
Elfogadási feltételek
- A mintafájl 2/3/7/8 statisztikát és A, majd ár sorrendű, 2 és 1 gyakoriságot ad; ismételt futáskor a kimenet bájtról bájtra azonos.
- A tényleges programfolyamat sikerkor 0, rossz argumentumszámnál 2, olvasási hibánál 1 állapotot ad, a dokumentált csatornák használatával.
- Az üres fájl négy nulla mezőt és üres gyakoriságlistát eredményez; az útvonal átvétele és a bemeneti fájl tartalma nem módosul.
Összefoglaló és továbblépés
Ezzel a miniprojekttel az adattípusok, gyűjtemények, iterátorok, hibakezelés és tesztek egy közös feladatban találkoztak. Következő lépésként emeld ki a számítást könyvtárba a korábbi tesztlecke mintájára, és rögzítsd a folyamatpróbákat is. Új kapcsolót csak új, pontosan megfogalmazott kimeneti és hibaszerződéssel adj hozzá, hogy a program továbbra is kiszámítható maradjon.