Het schrijven van een programma dat werkt als de Unix-tool "cat" is een goede oefening in het begrijpen van bestandsinvoer en -uitvoer, evenals het gebruik van argumenten in een command-line interface (CLI). In dit hoofdstuk gaan we een eenvoudige versie van de "cat"-tool bouwen die meerdere bestanden kan samenvoegen, en daarbij enkele basisopties implementeert, zoals het nummeren van de regels of het onderdrukken van lege regels.

Wat doet de "cat" tool?

De "cat" tool is in essentie een hulpmiddel om de inhoud van één of meerdere bestanden naar de standaard uitvoer (STDOUT) te sturen. Bijvoorbeeld, door het commando cat a b c > alles.txt uit te voeren, worden de inhoud van de bestanden a, b, en c aaneengeschakeld en weggeschreven naar een nieuw bestand genaamd alles.txt. Het originele gedrag van de tool is redelijk eenvoudig, maar biedt verschillende opties om de uitvoer aan te passen.

De BSD-versie van de tool heeft bijvoorbeeld een reeks opties zoals -n voor het nummeren van regels, of -b om alleen niet-lege regels te nummeren. Deze opties kunnen in een command-line programma worden geïmplementeerd door argumenten te verwerken en de gewenste bewerking toe te passen.

Het Basisprogramma Schrijven

De uitdaging is om een programma te schrijven dat de functionaliteit van de "cat" tool nabootst. Het eerste wat we moeten doen is de vereiste argumenten verwerken. Een belangrijke keuze in Rust is het gebruik van een Args struct voor het analyseren van de commandoregelargumenten. Dit stelt ons in staat om de input van de gebruiker goed te structureren en te verwerken. De Args struct bevat bijvoorbeeld een veld text van het type Vec<String> om de bestandsnamen vast te leggen, en een booleaanse waarde omit_newline om te bepalen of een nieuwe regel moet worden weggelaten bij de uitvoer.

Wanneer we deze argumenten ontvangen, kunnen we het juiste bestand openen of de standaardinvoer lezen, afhankelijk van wat de gebruiker heeft opgegeven. We gebruiken bijvoorbeeld std::fs::read_to_string om bestandinhoud te lezen, of als er een streepje - wordt ingevoerd, lezen we van de standaardinvoer.

Het volgende deel van het programma is het verwerken van de opties. We kunnen de vlag -n gebruiken om alle regels te nummeren, en de vlag -b om alleen de niet-lege regels te nummeren. Rust biedt ons de mogelijkheid om eenvoudig door de inhoud van bestanden te itereren, waarbij we de enumerate methode gebruiken om regels te nummeren. Vervolgens kunnen we deze regels naar de standaarduitvoer sturen, of de inhoud naar een nieuw bestand schrijven als de gebruiker dat wenst.

Het Gebruiken van Tests

Een belangrijk aspect van het bouwen van betrouwbare software is het testen van de functionaliteit. In dit geval kunnen we verschillende tests schrijven om te controleren of ons programma correct werkt. We kunnen bijvoorbeeld testen of het programma de juiste uitvoer genereert wanneer het wordt uitgevoerd met verschillende bestanden, en of het juiste gedrag vertoont bij het gebruik van de flags -n en -b.

Een test zou kunnen controleren of de uitvoer overeenkomt met wat we verwachten: bijvoorbeeld of de regels correct genummerd zijn, of dat lege regels worden genegeerd. Het testen van de invoer kan ook betekenen dat we controleren of het programma goed reageert op foutieve argumenten of ontbrekende bestanden. Dit biedt ons niet alleen de zekerheid dat het programma goed werkt, maar zorgt er ook voor dat we snel kunnen ingrijpen als er iets misgaat.

De Vereiste Opties Implementeren

Voor dit specifieke project richten we ons op enkele belangrijke opties van de cat tool. Zoals eerder vermeld, implementeren we de -n en -b opties om regels te nummeren, en zullen we ook de mogelijkheid toevoegen om lege regels te onderdrukken met de -s optie. De code zal de verschillende argumenten verwerken en de bijbehorende acties uitvoeren. Zo kunnen we de inhoud van een bestand of de standaardinvoer lezen, de juiste nummering toepassen, en vervolgens de uitvoer naar de standaarduitvoer sturen.

De specifieke opties die we implementeren kunnen het gedrag van de uitvoer aanzienlijk beïnvloeden. Bijvoorbeeld, met de -n optie nummeren we alle regels, terwijl we met -b alleen de niet-lege regels nummeren. Het gebruik van de -s optie kan ervoor zorgen dat we meerdere lege regels samenvoegen tot één lege regel in de uitvoer.

Bestanden en Standaardinvoer

In dit hoofdstuk leer je ook hoe je omgaat met bestandsinvoer en de standaardinvoer in een Rust-programma. Het lezen van bestanden kan eenvoudig worden gedaan met de functie std::fs::read_to_string, maar als de gebruiker een streepje - invoert, moet je van de standaardinvoer lezen. Dit kan worden gedaan door de std::io::stdin te gebruiken, wat ons in staat stelt om dynamisch gegevens van de gebruiker te ontvangen.

Daarnaast is het belangrijk om te begrijpen hoe je met bestands- of foutmeldingen omgaat. In Rust kun je eenvoudig controleren of een bestand bestaat met std::path::Path::exists, en kun je foutmeldingen afhandelen door de standaarderror (STDERR) te gebruiken, zoals in de functie eprintln!.

Wat Je Nog Meer Moet Begrijpen

Bij het schrijven van een programma dat de functionaliteit van een bestaande Unix-tool implementeert, is het belangrijk om niet alleen de basisopties te begrijpen, maar ook hoe deze opties met elkaar in wisselwerking staan. Bijvoorbeeld, als je meerdere vlaggen opgeeft, zoals -b en -n, wat gebeurt er dan? Welke vlag heeft prioriteit, en hoe kun je ervoor zorgen dat de uitvoer in dat geval correct is?

Je moet ook bedenken hoe gebruikers van je programma fouten kunnen maken in de invoer. Wat gebeurt er als een gebruiker een ongeldige bestandsnaam opgeeft, of als er een probleem is met de toegang tot een bestand? Het robuust afhandelen van fouten is een essentiële vaardigheid voor het schrijven van betrouwbare software.

Bovendien, als je dit programma in een groter project wilt gebruiken, is het belangrijk om de tests zo te schrijven dat ze gemakkelijk kunnen worden aangepast of uitgebreid. Dit betekent dat je ervoor zorgt dat je code modulair is en goed gestructureerd, zodat nieuwe functionaliteiten zonder al te veel moeite kunnen worden toegevoegd.

Hoe argumenten correct te verwerken in een Rust-programma voor bestandsinvoer en uitvoer

Het beheren van gebruikersinvoer in een Rust-programma kan een uitdagende taak zijn, vooral wanneer het gaat om het verwerken van verschillende argumenten die gebruikers aan het programma doorgeven. Dit is vooral relevant in situaties waarbij een programma zoals de head-tool, die de eerste paar regels van een bestand toont, geëmuleerd wordt. In dit geval wil het programma de mogelijkheid bieden om het aantal regels en bytes dat wordt weergegeven aan te passen, evenals om meerdere bestanden in één keer te verwerken.

Bij het ontwikkelen van dergelijke functionaliteit is het essentieel om de invoerparameters goed te definiëren en te valideren, zodat het programma robuust is en niet crasht bij ongeldige input. Hieronder wordt uitgelegd hoe je argumenten zoals het aantal regels, het aantal bytes, en de bestandspaden effectief kunt verwerken.

Het gebruik van de crate clap is een goede keuze om de argumenten te definiëren en te parseren. Deze crate biedt een uitgebreide en flexibele manier om de invoer van de gebruiker te beheren. In het specifieke voorbeeld wordt gebruikgemaakt van de Arg-struct uit clap om de verschillende argumenten te definiëren.

Argumenten definiëren met clap

Bij het definiëren van de argumenten moet je rekening houden met de volgende aspecten:

  • Aantal regels (--lines / -n): Dit argument geeft aan hoeveel regels van elk bestand moeten worden weergegeven. Het heeft een standaardwaarde van 10, maar kan door de gebruiker worden aangepast met een integer groter dan of gelijk aan 1.

  • Aantal bytes (--bytes / -c): Dit argument bepaalt hoeveel bytes van het bestand moeten worden weergegeven. Dit argument kan niet samen met het --lines-argument worden gebruikt, aangezien deze twee parameters elkaar uitsluiten.

  • Bestanden (--files): Dit is een positioneel argument dat de lijst van bestanden specificeert. Als er geen bestanden worden opgegeven, wordt het standaardbestand - gebruikt, wat betekent dat de invoer van de standaardinvoer (stdin) wordt gelezen.

In het volgende voorbeeld wordt het gebruik van clap getoond voor het definiëren van deze argumenten:

rust
fn get_args() -> Args {
let matches = Command::new("headr") .version("0.1.0") .author("Ken Youens-Clark") .about("Rust versie van 'head'") .arg( Arg::new("lines") .short('n') .long("lines") .value_name("LINES") .help("Aantal regels")
.value_parser(clap::value_parser!(u64).range(1..))
.
default_value("10"), ) .arg( Arg::new("bytes") .short('c') .long("bytes") .value_name("BYTES") .conflicts_with("lines") .value_parser(clap::value_parser!(u64).range(1..)) .help("Aantal bytes"), ) .arg( Arg::new("files") .value_name("FILE") .help("Invoerbestand(en)") .num_args(0..) .default_value("-"), ) .get_matches(); Args { files: matches.get_many("files").unwrap().cloned().collect(),
lines: matches.get_one("lines").cloned().unwrap(),
bytes: matches.
get_one("bytes").cloned(), } }

Het parseren van argumenten en validatie

De bovenstaande code toont de manier waarop de argumenten worden gedefinieerd. Het is belangrijk om te begrijpen hoe je deze invoer correct valideert en ervoor zorgt dat de invoer van de gebruiker voldoet aan de verwachte specificaties. Bijvoorbeeld:

  • Als de gebruiker een ongeldige waarde opgeeft voor --lines of --bytes, moet het programma een foutmelding genereren en stoppen. Dit kan worden bereikt met behulp van de functie clap::value_parser, die ervoor zorgt dat de waarden binnen een specifiek bereik vallen.

  • De optie -n (aantal regels) kan niet worden gebruikt in combinatie met de optie -c (aantal bytes), en als beide argumenten worden opgegeven, moet het programma een foutmelding tonen.

Een voorbeeld van een fout die kan optreden bij ongeldige invoer is:

bash
$ cargo run -- -n blargh tests/inputs/one.txt
error: invalid value 'blargh' for '--lines': invalid digit found in string

Bestanden openen en lezen

Nadat de argumenten zijn geparsed, moet het programma de opgegeven bestanden openen. Het gebruik van de BufReader en de standaardbibliotheek std::fs::File maakt dit eenvoudig. Het programma moet ook foutmeldingen afdrukken als bestanden niet gevonden worden. Hierbij wordt het volgende patroon gevolgd:

rust
fn open(filename: &str) -> Result<Box<dyn BufRead>> { match filename { "-" => Ok(Box::new(BufReader::new(io::stdin()))), _ => Ok(Box::new(BufReader::new(File::open(filename)?))), } }
fn run(args: Args) -> Result<()> {
for filename in args.files { match open(&filename) { Err(err) => eprintln!("{filename}: {err}"), Ok(_) => println!("Bestand {filename} geopend"), } } Ok(()) }

Deze code probeert elk bestand te openen. Als het bestand niet bestaat, wordt een foutmelding weergegeven; als het bestand succesvol wordt geopend, wordt een bericht afgedrukt.

Bestanden verwerken en output genereren

Nadat de bestanden zijn geopend, is de volgende stap het lezen van de inhoud van de bestanden en het weergeven van de gewenste regels of bytes. Het is belangrijk om hierbij de juiste foutafhandeling toe te passen en de uitvoer te formatteren zoals verwacht. Het originele head-programma voegt bijvoorbeeld een koptekst toe voor elk bestand en geeft vervolgens de inhoud van dat bestand weer. Ongewenste bestanden, zoals een niet-bestaand bestand, worden slechts gemeld als fout.

Het programma moet ook foutmeldingen tonen voor bestanden die niet kunnen worden geopend, bijvoorbeeld wanneer het bestand niet bestaat:

bash
head: blargh: No such file or directory

Door goed om te gaan met de bestandsinvoer en uitvoer, kan het programma de verwachte functionaliteit leveren zonder onverwachte crashes.

Hoe een bestand efficiënt lezen en verwerken: Praktische benaderingen in Rust

In de wereld van softwareontwikkeling, vooral bij het werken met bestanden, speelt de juiste verwerking van bestandinvoer een cruciale rol. Het is niet voldoende om gewoon bestanden te openen en hun inhoud te lezen; het proces moet robuust, flexibel en efficiënt zijn. Dit geldt zeker voor de taal Rust, die bekend staat om zijn prestaties en geheugenveiligheid. In dit hoofdstuk bespreken we de aanpakken en technieken die je kunt gebruiken om bestanden te lezen, hun inhoud te verwerken en mogelijke fouten af te handelen.

Allereerst komt de noodzaak om een getal op een specifieke manier te parseren. Stel dat je een getal hebt dat een positief of negatief teken heeft, en je wilt de negatieve versie van dit getal berekenen. Een manier om dit te doen is door het getal te parsen en vervolgens i64::wrapping_neg te gebruiken. Deze methode zorgt ervoor dat een positief getal wordt omgezet naar een negatief getal, terwijl een negatief getal ongewijzigd blijft. Dit zorgt voor een eenvoudige en consistente manier om negatieve waarden te berekenen, zonder dat je je zorgen hoeft te maken over overloop of andere numerieke problemen.

Wanneer je een bestand opent, wil je meestal niet alleen maar weten of het bestand toegankelijk is, maar ook hoeveel regels en bytes het bevat. Dit kan vooral belangrijk zijn wanneer je met grote bestanden werkt en bijvoorbeeld alleen de laatste paar regels wilt afdrukken. Je moet namelijk weten hoeveel regels er in het bestand staan om de juiste regel te kunnen bepalen. Rust biedt een robuuste manier om deze gegevens te verkrijgen. De functie count_lines_bytes speelt hierbij een belangrijke rol. Door het aantal regels en bytes van een bestand te tellen, kun je eenvoudig bepalen welke delen van het bestand je moet verwerken. De functie count_lines_bytes kan eenvoudig worden geïmplementeerd door gebruik te maken van Rust's ingebouwde bestandsbehandelingsmechanismen.

Natuurlijk is het ook belangrijk om rekening te houden met de mogelijkheid dat het bestand niet op de verwachte manier kan worden gelezen. Bijvoorbeeld, wanneer het bestand niet bestaat, of als het bestand leeg is, moet je op een verantwoorde manier omgaan met deze situaties. Hier komt de kracht van foutafhandelingsmechanismen in Rust naar voren. Door gebruik te maken van Result, kun je elegant fouten afhandelen zonder dat je de stabiliteit van je programma in gevaar brengt. Dit zorgt ervoor dat je programma robuust is, zelfs wanneer het bestand niet aanwezig is of een andere onvoorziene situatie optreedt.

Zodra je de bestandshandleiding hebt verkregen en de lijnen en bytes hebt geteld, is het tijd om daadwerkelijk met de gegevens te werken. Als je bijvoorbeeld slechts een specifiek aantal regels wilt afdrukken, moet je bepalen vanaf welke regel je moet beginnen. Dit is niet altijd rechttoe rechtaan, vooral als je negatieve getallen gebruikt om bijvoorbeeld vanaf het einde van het bestand te beginnen. Hier komt de functie get_start_index van pas. Deze functie berekent de juiste startindex, afhankelijk van de door de gebruiker opgegeven waarde, en zorgt ervoor dat je de juiste regels afdrukt. Het is belangrijk dat je in deze stap rekening houdt met scenario's waarin je meer regels of bytes wilt lezen dan het bestand daadwerkelijk bevat. In dat geval moet je het hele bestand afdrukken, of gewoon niets, afhankelijk van je instellingskeuzes.

Verder, wanneer je werkt met bestanden, is de prestaties altijd een belangrijke overweging. In plaats van het hele bestand in het geheugen te laden, kun je beter een stream van de gegevens lezen en verwerken. Dit voorkomt dat je te veel geheugen gebruikt, vooral als je werkt met grote bestanden. De functies print_lines en print_bytes implementeren dit principe. Beide functies zijn ontworpen om slechts een deel van het bestand te verwerken, afhankelijk van de vereisten van de gebruiker, zonder onnodig geheugen te verbruiken.

Naast het lezen van het bestand en het afdrukken van de gegevens, is het essentieel om robuuste tests te schrijven. In Rust kun je tests schrijven die ervoor zorgen dat je functies correct werken, zelfs wanneer het bestand niet bestaat of de inhoud niet aan de verwachtingen voldoet. Tests helpen je ervoor te zorgen dat je programma robuust is en goed presteert onder verschillende omstandigheden. In de praktijk zou je bijvoorbeeld testen moeten uitvoeren die ervoor zorgen dat het programma zich correct gedraagt bij het verwerken van lege bestanden, ongeldige bestandsnamen, of bestanden met verschillende formaten.

Een ander belangrijk aspect bij het verwerken van bestanden is de efficiëntie van het ophalen van gegevens. Het is niet altijd mogelijk om gegevens in één keer te lezen, vooral als je werkt met enorme bestanden. Het gebruik van functies die het mogelijk maken om alleen de benodigde delen van het bestand te lezen, bijvoorbeeld door een specifieke startpositie op te geven, is van groot belang. Dit zorgt ervoor dat je programma niet meer geheugen gebruikt dan nodig, terwijl je tegelijkertijd de juiste gegevens verwerkt.

Tot slot moet je, afhankelijk van de aard van je programma, zorgvuldig nadenken over de foutafhandelingsstrategie. Rust biedt uitstekende hulpmiddelen voor het effectief omgaan met fouten, maar het is belangrijk om een consistente en robuuste foutafhandelingslogica te ontwikkelen die je programma betrouwbaar houdt.

Hoe kan ik mijn programma testen met aangepaste exitcodes en output?

In Rust is het gebruikelijk om programma's te testen door hun exitstatus en uitvoer te verifiëren. Dit wordt vaak gedaan via testprogramma's die binnen de huidige crate worden uitgevoerd, zonder dat je de gebundelde uitvoerbare bestanden hoeft te verplaatsen. Het gebruik van crates zoals assert_cmd en pretty_assertions maakt dit proces eenvoudiger en krachtiger.

Om een programma te testen binnen een crate, kun je de crate assert_cmd gebruiken. Deze crate biedt een eenvoudige manier om een opdracht in de crate uit te voeren en te verifiëren of het correct wordt uitgevoerd, zonder het programma fysiek te kopiëren naar de testmap. Bovendien kun je met de crate pretty_assertions de verschillen tussen verwachte en werkelijke uitvoer beter zichtbaar maken dan met de standaard assert_eq! macro van Rust.

Als eerste stap voeg je deze crates toe aan de Cargo.toml als ontwikkelingsafhankelijkheden:

toml
[package]
name = "hello" version = "0.1.0" edition = "2021" [dev-dependencies] assert_cmd = "2.0.13" pretty_assertions = "1.4.0"

Met deze dependencies kan je beginnen met het testen van je programma. In je testbestand, bijvoorbeeld tests/cli.rs, maak je gebruik van de Command van de crate assert_cmd om de uitvoerbare bestanden van de huidige crate te vinden. Het onderstaande voorbeeld toont hoe je een eenvoudige test schrijft die controleert of de uitvoering van een programma succesvol is:

rust
use assert_cmd::Command;
#[test] fn runs() { let mut cmd = Command::cargo_bin("hello").unwrap(); cmd.assert().success(); }

Wat gebeurt hier? De Command::cargo_bin("hello") zoekt naar het binaire bestand hello in de huidige crate en voert het uit. Het resultaat van de uitvoering wordt vervolgens gecontroleerd met de assert().success() methode, die verifieert dat het programma zonder fouten wordt uitgevoerd. Als het programma niet succesvol is, zal de test falen.

Deze test geeft geen garanties over de specifieke uitvoer van het programma, maar enkel dat het programma succesvol wordt uitgevoerd. Het is ook belangrijk om te weten dat de unwrap() functie wordt gebruikt om een paniek te veroorzaken als het binaire bestand niet kan worden gevonden, wat ervoor zorgt dat de test faalt wanneer er een probleem is met de binaire bestanden.

Wat betekent een succesvolle uitvoering?

In de context van commando-line programma's, is de exitcode van het programma van groot belang. Het POSIX-standaard vereist dat een programma een exitstatus van 0 retourneert wanneer het succesvol wordt uitgevoerd en een waarde tussen 1 en 255 wanneer er een fout optreedt. Dit wordt vaak gebruikt om te communiceren of een programma correct heeft gewerkt of niet.

Neem bijvoorbeeld de true en false commando’s. Het true commando heeft altijd een exitcode van 0:

bash
$ true
$ echo $? 0

Daarentegen heeft false altijd een exitcode van 1:

bash
$ false $ echo $? 1

Je kunt zelf een eenvoudige versie van deze commando’s maken in Rust door gebruik te maken van std::process::exit om expliciet een exitstatus te retourneren. Maak een bestand src/bin/true.rs en voeg het volgende toe:

rust
fn main() {
std::process::exit(0); }

Je kunt dit vervolgens testen door het binaire bestand uit te voeren:

bash
$ cargo run --quiet --bin true $ echo $? 0

De test voor deze functionaliteit in Rust zou er als volgt uitzien:

rust
#[test]
fn true_ok() { let mut cmd = Command::cargo_bin("true").unwrap(); cmd.assert().success(); }

Wanneer je de tests uitvoert, zou je een succesmelding moeten zien. Deze test verifieert dat het programma succesvol uitvoert en een exitcode van 0 retourneert.

Testen van foutscenario's

Voor het testen van foutscenario’s kun je een programma schrijven dat een niet-nul exitstatus retourneert. Het volgende voorbeeld toont hoe je het false commando kunt implementeren:

rust
fn main() { std::process::exit(1); }

Het uitvoeren van dit programma zal een foutcode genereren:

bash
$ cargo run --quiet --bin false
$ echo $? 1

Je kunt een test toevoegen om ervoor te zorgen dat het programma daadwerkelijk faalt:

rust
#[test] fn false_not_ok() { let mut cmd = Command::cargo_bin("false").unwrap(); cmd.assert().failure(); }

Wanneer je deze test uitvoert, zal het programma niet succesvol zijn, wat wordt aangegeven door de assert().failure() methode. Dit helpt bij het valideren van het foutafhandelingsmechanisme van je programma.

Het testen van de uitvoer

Hoewel het belangrijk is om te controleren of een programma succesvol wordt uitgevoerd, is het ook van belang te verifiëren of het programma de juiste uitvoer produceert. Je kunt de uitvoer van het programma vastleggen en vergelijken met de verwachte uitvoer.

Bijvoorbeeld, stel dat je het programma hello hebt dat de tekst "Hello, world!" afdrukt naar de standaard uitvoer. Je zou een test kunnen schrijven zoals hieronder:

rust
use assert_cmd::Command;
use pretty_assertions::assert_eq; #[test] fn runs() { let mut cmd = Command::cargo_bin("hello").unwrap(); let output = cmd.output().expect("fail"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8"); assert_eq!(stdout, "Hello, world!\n"); }

Deze test voert het programma uit, verkrijgt de uitvoer, en vergelijkt deze met de verwachte tekst "Hello, world!". Als de uitvoer verschilt, zal de test falen. Dit biedt niet alleen een controle op de exitcode, maar ook op de daadwerkelijke uitvoer van het programma.

Veranderingen in de uitvoer

Wanneer je de uitvoer van het programma aanpast, bijvoorbeeld door extra uitroeptekens toe te voegen, zal de test falen:

rust
fn main() { println!("Hello, world!!!"); }

Bij het uitvoeren van de test krijg je een foutmelding die aangeeft waar de verwachte uitvoer verschilt van de daadwerkelijke uitvoer. Dit helpt om nauwkeurig te controleren of het programma zich gedraagt zoals verwacht.

Conclusie

Het testen van programma's in Rust kan eenvoudig worden uitgevoerd door gebruik te maken van de juiste tools en technieken. Het is belangrijk niet alleen de succesvolle uitvoering van een programma te verifiëren, maar ook de uitvoer en de exitstatus. Het gebruik van crates zoals assert_cmd en pretty_assertions maakt het testen eenvoudiger en biedt nuttige hulpmiddelen om nauwkeurige tests te schrijven.