I programmering er det essensielt å kunne håndtere filer på en pålitelig måte, spesielt når det gjelder å unngå feil som kan oppstå når man prøver å lese fra eller skrive til en fil. I Rust, et språk kjent for sin strenge typekontroll og minnehåndtering, er filoperasjoner ikke unntatt fra dette ansvaret. Et viktig aspekt ved Rust-programmering er å forstå hvordan man effektivt håndterer feil som kan oppstå under filbehandling, og hvordan man kan bruke Rusts egenskaper som eierskap og referanser for å sikre at programmene fungerer korrekt uten å introdusere minnelekkasjer.
En funksjon som genererer et tilfeldig filnavn som ikke finnes, er et godt eksempel på dette. Funksjonen gen_bad_file kan brukes til å skape et filnavn som ikke eksisterer i filsystemet, og den returnerer en String med et tilfeldig valgt filnavn. Denne funksjonen begynner med å generere et tilfeldig alfanumerisk filnavn med syv tegn ved hjelp av rand::thread_rng() og en iterator som trekker tilfeldig fra alfanumeriske tegn. Deretter sjekker den om filen eksisterer ved hjelp av fs::metadata, som returnerer en feil hvis filen ikke finnes. Hvis det er tilfellet, returneres filnavnet.
I dette eksemplet benyttes eierskap og låneprinsipper i Rust på en interessant måte. Den første bruken av filename skjer gjennom en låning med referanse (&filename), som ikke endrer eierskapet til verdien. Den andre gangen, derimot, beveges eierskapet til filename til funksjonen fs::metadata, og dermed får vi en kompileringstidfeil hvis vi prøver å bruke filename etter at det er blitt "flyttet" i minnet. Dette er et viktig konsept i Rust, ettersom det er nødvendig å forstå når verdier blir flyttet og når de bare blir lånt.
Dette leder oss til et annet viktig aspekt i filoperasjoner: feilhåndtering og hvordan programmet reagerer når en fil ikke kan åpnes eller leses. I den tidligere beskrivelsen av funksjonen run ser vi hvordan Rust håndterer resultater fra filoperasjoner. Når programmet forsøker å åpne en fil, bruker det en match for å håndtere både suksess og feiltilfeller. Ved feil vil en feilmelding skrives ut til standardfeil (STDERR), mens programmet fortsetter å forsøke å åpne de gjenværende filene.
Videre kan vi legge til mer spesifik handling for feil, som for eksempel å lese linjer fra en fil ved hjelp av BufRead::lines. Når vi leser linjer fra en fil, får vi et Result, et type som håndterer potensielle feil under I/O-operasjoner. Denne feilhåndteringen er spesielt viktig, ettersom operasjoner som lesing og skriving til fil kan feile av mange årsaker, som for eksempel filsystemfeil eller utilstrekkelige tillatelser.
Et annet nyttig aspekt i Rust-programmering er muligheten til å håndtere standardinn- og utdata. Funksjonen run_stdin er et godt eksempel på hvordan man kan sende inn data via standard inndata (STDIN) og få utdata via standard utdata (STDOUT). Den leser innholdet i en fil som input, sender det som STDIN til et program, og sammenligner deretter programutgangen med et forventet resultat. Denne tilnærmingen kan brukes til å teste hvordan programmet håndterer ulike innganger, og gir en robust metode for testing og validering av programmet.
I tillegg til dette kan vi legge til muligheten for å vise linjenumre i programutgangen, noe som ofte er nyttig for verktøy som skal behandle tekstfiler. Ved å legge til et -n eller --number alternativ kan vi telle linjene i en fil og vise linjenumrene sammen med innholdet. Dette gjøres ved å bruke en enkel teller som oppdateres for hver linje som leses, og ved å skrive ut linjenumrene sammen med selve linjen.
Rust gir programmerere flere verktøy for å skrive trygge, effektive og pålitelige applikasjoner, spesielt når det gjelder filbehandling og feilhåndtering. Å forstå hvordan man bruker eierskap, låning, feilhåndtering og testing i Rust kan bidra til å skape robuste programmer som håndterer alle mulige feil på en effektiv måte.
Hvordan kjøre og teste Rust-programmer effektivt ved hjelp av Cargo og CLI
I Rust er Cargo det primære verktøyet for å bygge og administrere prosjekter. Når du kjører et Rust-program ved hjelp av Cargo, kan du se at det automatisk setter opp en struktur med flere viktige filer og mapper. Når du kjører et program via Cargo, vil du også legge merke til at programmet viser en "usage statement" (brukermelding) som forklarer hvordan du kan bruke det.
Etter å ha kjørt programmet med Cargo, kan du bruke kommandoen ls for å liste innholdet i den gjeldende arbeidskatalogen. Dette vil avsløre en ny mappe kalt target, som er der Cargo lagrer byggeartefakter. Som standard bygger Cargo et "debug"-mål, så du vil finne en mappe som heter target/debug som inneholder programfiler som er generert under byggingen. For å vise dette, kan du skrive følgende kommando i terminalen:
I denne katalogen finner du programmet som ble kjørt, for eksempel target/debug/hello. Du kan deretter kjøre dette direkte ved å bruke:
Her finner du den binære filen som Cargo har bygget, og du kan enkelt se hvordan programmet ditt fungerer. Hvorfor heter den binære filen hello og ikke main? Svaret ligger i Cargo.toml-filen, som spesifiserer navnet på prosjektet, som også blir navnet på den utførbare filen:
Dette er navnet på prosjektet som ble opprettet med Cargo, og derfor blir det også navnet på den utførbare filen. Filen definerer også hvilken versjon av Rust som skal brukes for å bygge prosjektet, og dette kan endres etter behov.
Når det gjelder testing av programmer, er det to hovedtyper tester som bør implementeres: enhetstesting og integrasjonstesting. Enhetstesting er når du skriver tester for de enkelte funksjonene i programmet, mens integrasjonstesting innebærer at du skriver tester som kjører programmet på samme måte som en bruker ville gjort, altså som en "black-box" test. For dette formålet kan vi opprette en egen mappe kalt tests parallelt med src-mappen for å holde testkoden.
For eksempel kan du lage en enkel test for å verifisere om programmet ditt kjører kommandoen ls ved å opprette en fil i tests/cli.rs med følgende kode:
Denne testen bruker assert!-makroen for å bekrefte at et boolsk uttrykk er sant. I dette tilfellet er uttrykket alltid sant, så testen vil alltid passere. Du kan kjøre testene med cargo test, og du vil få utdata som ser slik ut:
For å gjøre testen mer relevant, kan vi i stedet teste om programmet kan kjøre kommandoen ls ved å bruke std::process::Command. Denne modulen lar deg utføre eksterne kommandoer fra Rust-programmet ditt. Her er et eksempel på en test som verifiserer at ls kjører som forventet:
I denne testen lager vi en ny Command for å kjøre ls og verifiserer at kommandoen ble kjørt vellykket ved å sjekke om resultatet er en Ok-variant. Etter å ha kjørt cargo test, vil du se at testen passer:
Hvis du i stedet prøver å kjøre programmet ditt, for eksempel hello, kan du endre testen til å se slik ut:
Når du kjører testen på nytt, vil den mislykkes fordi programmet hello ikke finnes i PATH-en din. Dette er fordi operativsystemet ditt kun ser etter kommandoer i spesifikke kataloger som er definert i miljøvariabelen PATH. Hvis du prøver å kjøre hello fra terminalen, vil du få følgende feilmelding:
For å løse dette kan du referere til den eksakte banen til programmet ved å bruke ./hello, noe som gir:
Rusts standard praksis er å bruke target/debug/ som stedet der binære filer bygges. Men for å kunne kjøre disse filene som kommandoer fra hvilken som helst katalog, må de enten legges til i PATH eller refereres til med den absolutte banen.
Når du lager prosjekter i Rust, er det viktig å også forstå hvordan Cargo håndterer avhengigheter. Hvis du for eksempel bruker eksterne pakker (kalt "crates"), vil Cargo automatisk laste ned og kompilere disse. Crates bruker semantisk versjonering, der store endringer kan bryte bakoverkompatibiliteten. Versjonsnummer som 1.2.4 betyr at hovedversjonen er 1, den mindre versjonen er 2, og oppdateringen er 4. En endring i hovedversjonen indikerer en inkompatibel API-endring.
Integrasjonstesting er et effektivt verktøy for å sikre at programmet ditt fungerer som forventet fra brukerens perspektiv. Dette er en viktig del av arbeidsprosessen som forhindrer feil tidlig i utviklingssyklusen. Husk at testing ikke bare avdekker feil som allerede eksisterer i koden, men gir også innsikt som kan hjelpe deg å designe bedre løsninger fremover.
Hvordan håndtere kommandolinjeargumenter for Rust-programmer?
I Rust-programmer er håndtering av kommandolinjeargumenter en viktig ferdighet for å lage fleksible og brukervennlige applikasjoner. En sentral del av dette er å bruke argumenter som lar brukeren tilpasse hvordan programmet kjøres, som i eksempelet med programmet "tailr". Dette programmet etterligner funksjonaliteten til det velkjente Unix-verktøyet tail, som brukes til å vise de siste linjene i en fil.
Kommandolinjegrensesnittet (CLI) for dette programmet benytter seg av flere argumenter som lar brukeren spesifisere hvilke filer som skal leses, hvor mange linjer eller byte som skal vises, og om header-informasjon skal undertrykkes. Her er en grundig gjennomgang av hvordan disse argumentene håndteres og valideres i Rust, med hjelp av biblioteket clap.
Argumenter for programmet
Programmet aksepterer flere typer argumenter:
-
Filargumenter: Dette argumentet er obligatorisk og spesifiserer hvilke filer som skal leses. Brukeren kan spesifisere flere filer, og programmet vil håndtere dem i den rekkefølgen de er oppgitt. Argumentet er definert som et posisjonelt argument i
clap, noe som betyr at det må komme før eventuelle flagg eller alternativer. -
Antall linjer: Brukeren kan spesifisere hvor mange linjer fra slutten av filen som skal vises. Dette gjøres med flagget
--lineseller den kortere formen-n. Standardverdien er 10, så hvis brukeren ikke spesifiserer antall linjer, vises de siste 10 linjene. -
Antall byte: I stedet for å vise linjer, kan brukeren spesifisere antall byte som skal vises fra slutten av filen med flagget
--byteseller-c. Dette argumentet kan ikke brukes sammen med--lines, ettersom de to alternativene er gjensidig utelukkende. -
Undertrykke header: Brukeren kan velge å undertrykke headerinformasjonen ved å bruke flagget
--quieteller-q. Når dette flagget er satt, vises ikke informasjon om hvilke filer som blir behandlet.
Når programmet kjøres med disse argumentene, vil det validere inngangsverdiene. Hvis brukeren forsøker å kombinere inkompatible argumenter, som å spesifisere både linjer og byte, vil programmet returnere en feilmelding.
Håndtering av argumenter i Rust
For å implementere håndteringen av disse argumentene, brukes biblioteket clap. Dette biblioteket tilbyr en enkel og effektiv måte å definere og validere kommandolinjeargumenter på.
Her er et eksempel på hvordan argumentene kan defineres:
I dette eksempelet er argumentene definert med clap::Arg, og de viktigste alternativene (filer, linjer, byte og stille-modus) er spesifisert. Det er også definert hvordan disse argumentene skal valideres og håndteres, som for eksempel at --bytes og --lines er gjensidig utelukkende.
Validering og parsing av numeriske argumenter
En viktig utfordring i programmer som håndterer kommandolinjeargumenter er å validere og tolke numeriske verdier. I dette tilfellet er både linjene og byte-argumentene numeriske, og brukeren kan spesifisere både positive og negative verdier. For å håndtere dette benyttes en enum som representerer forskjellige typer numeriske verdier.
I denne enum-en representerer TakeValue::PlusZero spesialtilfellet for +0, som betyr at alle linjer eller byte skal vises. TakeValue::TakeNum(i64) representerer en spesifikk numerisk verdi, som kan være både positiv eller negativ. For å håndtere parsing av slike verdier kan en funksjon som parse_num skrives:
Denne funksjonen forsøker å tolke den innkommende strengen som enten et spesialtilfelle (+0) eller som en gyldig heltallsverdi. Hvis parsing mislykkes, returneres en feilmelding.
Viktige betraktninger
I tillegg til å forstå hvordan argumentene håndteres, er det viktig å merke seg noen punkter:
-
Standardverdier og feilbehandling: Når programmet ikke får spesifisert argumenter (som antall linjer), bruker det en standardverdi. Det er viktig at standardverdier er gjennomtenkte og fungerer i de fleste tilfeller, men det er også viktig å håndtere feil på en god måte når brukeren spesifiserer ugyldige verdier.
-
Brukervennlighet: God feilmelding og dokumentasjon er avgjørende for at brukeren skal forstå hvordan de kan bruke programmet korrekt. Å tilby en hjelpetekst (
--help) og versjonsinformasjon (--version) gir et ekstra lag av brukervennlighet. -
Testdekning: Å inkludere tester for parsing og validering av kommandolinjeargumenter er viktig for å sikre at programmet fungerer som forventet under forskjellige forhold.
-
Effektivitet: Når man håndterer store filer eller mange argumenter, kan det være lurt å bruke effektive metoder for å lese og behandle dataene, som å bruke buffers for innlesing av store filer i stedet for å laste alt inn i minnet på én gang.
Hvordan håndtere filsystemet i Rust med CLI-verktøy
I utviklingen av et kommandolinjeverktøy for filhåndtering er det viktig å ha en grundig forståelse av hvordan man interagerer med filsystemet, både når det gjelder å liste filer og sjekke deres egenskaper. I denne delen av boken vil vi dykke inn i hvordan man kan bygge et Rust-program som kan liste filer og mapper, samt hvordan man kan filtrere og vise skjulte filer.
For å komme i gang er det nødvendig å ha en struktur for argumenthåndtering som lar oss spesifisere hvilke filer og mapper som skal vises, om visningen skal være lang, og om skjulte filer skal inkluderes. Ved hjelp av clap-biblioteket kan vi enkelt definere kommando-linje-argumentene og deres standardverdier. Dette kan gjøres ved å sette opp et grunnleggende program som bruker følgende funksjon for å hente argumentene:
Denne funksjonen sørger for at vi kan spesifisere hvilke filer som skal vises, om vi skal bruke en lang visning, og om skjulte filer skal inkluderes i resultatet. Den gir oss en struktur kalt Args som vi kan bruke videre i programmet.
Filbehandling: Finne og filtrere filer
Når vi har fått inn argumentene, må vi finne filene og mappene som samsvarer med brukerens inndata. Dette krever en funksjon som kan iterere gjennom spesifiserte stier og returnere en liste over eksisterende filer og mapper. Vi kan bruke std::fs::metadata for å sjekke om en fil eller mappe eksisterer, og deretter bruke fs::read_dir for å lese innholdet i en mappe. Her er et eksempel på hvordan man kan implementere funksjonen:
I denne funksjonen sjekker vi først om stien er en fil eller en mappe. Hvis det er en fil, legger vi den til i resultatet. Hvis det er en mappe, bruker vi fs::read_dir for å hente innholdet i mappen og iterere gjennom det. Hvis flagget show_hidden er satt til true, inkluderes skjulte filer (de som begynner med en prikk), ellers hoppes de over.
Enhetstesting og håndtering av skjulte filer
For å sikre at programmet vårt fungerer som forventet, bør vi implementere enhetstester som verifiserer at vi får riktig resultat av funksjonene våre. For eksempel kan vi teste at funksjonen find_files returnerer de riktige filene, både når skjulte filer er inkludert og når de ikke er det. Her er et eksempel på hvordan slike tester kan se ut:
I testene ovenfor sjekker vi to scenarier: ett der skjulte filer ikke er inkludert, og ett der de er inkludert. Det er viktig å merke seg at filene kan returneres i forskjellig rekkefølge avhengig av operativsystemet, så vi sorterer resultatene før vi sammenligner dem.
Viktige hensyn ved implementering
Når du utvikler et program som håndterer filer og mapper, er det flere viktige faktorer å ta hensyn til:
-
Håndtering av feil: Sørg for at programmet ditt gir klare feilmeldinger når filer eller mapper ikke finnes. Dette er viktig for at brukeren skal forstå hva som gikk galt.
-
Ytelse: Når du leser inn innholdet i mapper, vær oppmerksom på ytelsen. For store mapper kan det være lurt å implementere funksjonalitet for å håndtere store datamengder effektivt, kanskje ved å bruke iterators for å unngå å laste alt inn i minnet samtidig.
-
Plattformspesifikasjoner: Filhåndtering kan variere mellom ulike operativsystemer, spesielt når det gjelder håndtering av skjulte filer og mapper. Vær oppmerksom på at skjulte filer på Unix-lignende systemer begynner med en prikk, mens på Windows kan det være andre kriterier som gjelder.
-
Brukergrensesnitt: Tenk på hvordan brukeren interagerer med programmet ditt. Det kan være nyttig å tilby flere alternativer for visning og filtrering av filer, slik at verktøyet kan tilpasses forskjellige behov.

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский