For å bygge et Rust-program som etterligner funksjonaliteten til Unix-kommandoen wc (word count), må vi først definere de nødvendige strukturene og argumentene som skal brukes til å håndtere inn- og utdata. Vi benytter oss av clap for å behandle kommandolinjeargumentene og BufReader for å åpne filer og lese dem. Her er en grundig gjennomgang av hvordan dette kan gjøres i Rust.

I første omgang må vi definere strukturen Args, som holder styr på argumentene som skal tas imot fra kommandolinjen. Ved hjelp av #[derive(Debug, Parser)] kan vi enkelt generere en parser for strukturen ved hjelp av clap-biblioteket. Her er et eksempel på hvordan Args kan se ut:

rust
#[derive(Debug, Parser)] #[command(author, version, about)] struct Args { #[arg(value_name = "FILE", default_value = "-")] files: Vec<String>, #[arg(short, long)] lines: bool, #[arg(short, long)] words: bool, #[arg(short('c'), long)] bytes: bool, #[arg(short('m'), long, conflicts_with("bytes"))] chars: bool, }

I denne strukturen kan vi spesifisere flere flagg, som angir hvilke data som skal telles i filene: linjer, ord, byte og tegn. En viktig detalj er at chars flagget er satt til å være i konflikt med bytes, så man kan ikke be om begge på samme tid.

Når vi har definert argumentene, må vi implementere logikken for å håndtere filene og telle elementene. I funksjonen run, sjekker vi om noen av flaggene er satt. Hvis ingen av flaggene er aktivert, setter vi automatisk linjer, ord og byte til å være sanne.

rust
fn run(mut args: Args) -> Result<()> {
if [args.words, args.bytes, args.chars, args.lines] .iter() .all(|v| v == &false) { args.lines = true; args.words = true; args.bytes = true; } println!("{args:#?}"); Ok(()) }

I denne koden oppretter vi en midlertidig liste med alle flaggene og bruker Iterator::all for å sjekke om alle er false. Hvis det er tilfelle, settes de til true automatisk. Dette er et eksempel på hvordan Rust kan bruke iterator-metoder for å gjøre koden både kortfattet og lesbar.

Arbeide med filer og telle innhold

Neste steg er å åpne filene og telle elementene deres. For dette bruker vi en funksjon open, som åpner filene basert på deres navn. Vi håndterer også feilmeldinger hvis en fil ikke kan åpnes:

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)?))), } }

Funksjonen run blir deretter utvidet for å åpne de oppgitte filene og vise meldinger hvis de enten åpnes eller ikke kan åpnes:

rust
fn run(mut args: Args) -> Result<()> { for filename in &args.files { match open(filename) { Err(err) => eprintln!("{filename}: {err}"), Ok(_) => println!("Opened {filename}"), } } Ok(()) }

Når filene er åpnet, kan vi begynne å telle linjer, ord, byte og tegn. Dette gjøres i funksjonen count, som tar en filhåndtak og returnerer et resultat med informasjon om antall linjer, ord, byte og tegn.

rust
fn count(mut file: impl BufRead) -> Result<FileInfo> {
let mut num_lines = 0; let mut num_words = 0; let mut num_bytes = 0; let mut num_chars = 0; Ok(FileInfo { num_lines, num_words, num_bytes, num_chars, }) }

Her er count-funksjonen fleksibel, da den tar et BufRead-implementeringsobjekt som kan være alt fra en fil til en in-memory buffer. Den returnerer et FileInfo-objekt som holder styr på tellingen av de ulike elementene.

Testing av funksjonalitet

For å sikre at programmet fungerer som forventet, kan vi skrive enhetstester. Disse testene kan hjelpe oss med å verifisere at count-funksjonen fungerer korrekt når den jobber med forskjellige filformater og innhold. I testene bruker vi en Cursor for å simulere filer i minnet, noe som er nyttig for testing uten å måtte lese ekte filer.

rust
#[cfg(test)]
mod tests { use super::{count, FileInfo}; use std::io::Cursor; #[test] fn test_count() { let text = "I don't want the world.\nI just want your half.\r\n"; let info = count(Cursor::new(text)); assert!(info.is_ok()); let expected = FileInfo { num_lines: 2, num_words: 10, num_chars: 48, num_bytes: 48, }; assert_eq!(info.unwrap(), expected); } }

Testen ovenfor sjekker at når vi gir en streng med tekst til count-funksjonen, får vi de riktige verdiene for linjer, ord, tegn og byte.

Hva man bør vite

For å fullt ut forstå hvordan programmet fungerer, er det viktig å merke seg noen ting. For det første bør man være kjent med Rusts mekanismer for håndtering av feil (som i Result-typen) og hvordan man bruker iteratorer til å gjøre operasjoner på samlinger av data. Videre er det viktig å forstå hvordan vi kan bruke traiten BufRead til å lese fra både filer og minnebaserte buffere. Denne fleksibiliteten gir oss muligheten til å håndtere forskjellige typer inngangsdata på en enkel og effektiv måte.

Når man jobber med systemer som dette, er det også essensielt å forstå hvordan man kan håndtere konfliktende flagg, som for eksempel chars og bytes, og hvordan man kan sette standardverdier når ingen flagg er spesifisert.

Hvordan håndtere kommandolinjeargumenter i Rust

Når du utvikler programmer i Rust, kan du ofte trenge å håndtere kommandolinjeargumenter som blir sendt til programmet ditt ved kjøring. Dette kan være nyttig for å gi programmet dynamisk funksjonalitet, som for eksempel å endre atferd basert på brukerens inndata. I denne artikkelen går vi gjennom hvordan du kan hente og bruke disse argumentene i et enkelt Rust-program.

I dette eksemplet vil vi lage et program som tar argumenter fra kommandolinjen og skriver dem ut. Vi bruker Cargo, Rusts pakkebehandler og byggeverktøy, for å opprette et nytt prosjekt og kjøre programmet.

Når du starter et nytt prosjekt med Cargo, kan du gjøre dette ved å kjøre følgende kommando i terminalen:

cpp
$ cargo new echor

Denne kommandoen oppretter et nytt prosjekt med navnet echor, og inneholder en filstruktur som ser slik ut:

shell
$ cd echor
$ tree . ├── Cargo.toml └── src └── main.rs

Du kan deretter bruke Cargo til å bygge og kjøre programmet:

arduino
$ cargo run

Standardprogrammet skriver ut "Hello, world!", som du har sett i første kapittel. Koden i src/main.rs ser slik ut:

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

Som vi har sett tidligere, starter Rust programmet ved å utføre funksjonen main i filen src/main.rs. Alle funksjoner i Rust returnerer en verdi, og main-funksjonen er et spesielt tilfelle. Den returnerer en såkalt enhetstype (unit type), som er representert med parentesene ().

En viktig egenskap ved funksjoner i Rust er at de kan returnere verdier. Hvis en funksjon ikke har et eksplisitt returtype, betyr det at den returnerer enhetstypen. Dette er et tomt eller meningsløst resultat, som ikke kan sammenlignes med en null-pekere i andre programmeringsspråk.

Hente kommandolinjeargumenter

Nå skal vi se på hvordan vi kan hente og vise kommandolinjeargumenter i Rust. Rust tilbyr en funksjon kalt std::env::args som kan brukes til dette formålet. Denne funksjonen returnerer en struktur av typen Args, som representerer kommandolinjeargumentene.

La oss begynne med å prøve å bruke denne funksjonen i vårt program. Hvis vi prøver å skrive ut argumentene direkte, som vist nedenfor, vil vi få en kompileringfeil:

rust
fn main() {
println!(std::env::args()); // Dette vil ikke fungere }

Komponenten gir følgende feil:

lua
error: format argument must be a string literal

Denne feilen oppstår fordi println!-makroen forventer en formatstreng for å kunne plassere verdien av argumentene. For å løse dette, må vi bruke et format-argument med {} eller {:?}, som indikerer at vi ønsker å bruke en Debug-utskrift for å vise verdien.

Etter å ha gjort endringen til:

rust
fn main() {
println!("{:?}", std::env::args()); // Nå fungerer det!
}

Vil programmet nå kompilere og kjøre uten problemer. Når du kjører programmet med:

arduino
$ cargo run Hello world

Vil du se følgende utdata:

css
Args { inner: ["target/debug/echor", "Hello", "world"] }

Her ser vi at de første argumentene er relatert til stien til det kjørende programmet. Dette er et vanlig fenomen i Rust (og andre programmeringsspråk), hvor programmet alltid får sitt eget filnavn som det første argumentet.

Arbeide med flags og posisjonelle argumenter

Kommandolinjeargumentene kan deles inn i to hovedtyper: posisjonelle argumenter og flags. Posisjonelle argumenter er de som er spesifikke for programmet, og deres rekkefølge er viktig. For eksempel, når du bruker programmet echo, er argumentene de tekstene som skal skrives ut, og deres rekkefølge bestemmer hvordan de vises.

Flags, derimot, er valgfri argumenter som vanligvis brukes for å endre programmets oppførsel. Et vanlig eksempel på en flagg er -n i programmet echo, som forteller programmet å ikke legge til et linjeskift etter utskriften. Du kan bruke Cargo til å sende flags ved å separere Cargo-kommandoens egne alternativer med to bindestreker, som vist her:

arduino
$ cargo run -- -n Hello world

Resultatet vil være:

css
Args { inner: ["target/debug/echor", "-n", "Hello", "world"] }

Som nevnt tidligere, er det vanlig at programflags (som -n) er korte alternativer med ett tegn (som -h for hjelp), men du kan også bruke mer beskrivende flagg med to bindestreker (som --help). Slike flags påvirker hvordan programmet kjører.

I vårt tilfelle, hvis vi implementerer en -n flagg, kan vi velge å fjerne linjeskiftet ved å bruke noe slikt som:

rust
fn main() {
let args: Vec<String> = std::env::args().collect(); let print_newline = !args.contains(&"-n".to_string()); if print_newline { println!("{}", args[1]); } else { print!("{}", args[1]); } }

Dette vil skrive ut teksten uten å legge til linjeskift hvis -n er spesifisert.

Viktige betraktninger

Når du håndterer kommandolinjeargumenter, er det viktig å huske på hvordan ulike kommandolinjeverktøy tolker argumenter. Generelt sett:

  • Programflagg (som -n) endrer programoppførselen, men de tar ikke nødvendigvis en verdi.

  • Posisjonelle argumenter (som Hello og world i eksemplet vårt) brukes i den rekkefølgen de vises i kommandolinjen.

  • Argumentene sendes til programmet som en liste, hvor det første elementet er alltid programnavnet.

Husk at feilaktig plassering av argumenter eller manglende behandling av spesifikke flags kan føre til uventede resultater. Det er også viktig å skille Cargo-kommandoens alternativer fra programmets egne argumenter ved å bruke --.