I denne delen vil vi lage et program som etterligner wc-kommandoen, som stammer fra den tidlige versjonen av AT&T Unix. Programmet skal telle antall linjer, ord og byte i tekst som enten leses fra standard input (STDIN) eller fra én eller flere filer. Dette er et viktig verktøy som brukes til å analysere tekstfilers størrelse og innhold, og blir ofte brukt for å telle linjene i resultatene fra andre prosesser. For å forstå hvordan vi kan implementere et slikt program, er det viktig å begynne med en kort forklaring av hvordan wc fungerer i praksis.

Hvordan wc fungerer

Kommandoen wc (word count) tar som standard inn tekst enten fra en fil eller fra standard input (STDIN). Den skriver ut antallet linjer, ord og byte som finnes i den gitte teksten. Dette fungerer på følgende måte:

  • Linjer: En linje er definert som en sekvens av tegn som avsluttes med et linjeskift (newline). Eventuelle tegn etter det siste linjeskiftet er ikke med i linjetellingen.

  • Ord: Et ord er en sekvens av tegn som er adskilt med mellomrom eller andre hvite tegn, som for eksempel tabulatorer eller linjeskift.

  • Bytes: Antall byte refererer til den faktiske mengden data i filen eller tekststrømmen. For ASCII-tegn vil dette tilsvare antallet tegn, men for Unicode-tegn kan det være flere bytes per tegn.

Kommandoen kan ta flere flagg som justerer hva som vises:

  • -c viser antall byte.

  • -l viser antall linjer.

  • -m viser antall tegn (i noen versjoner kan dette være lik antall byte, men i Unicode kan det være forskjellige verdier).

  • -w viser antall ord.

Når ingen spesifikke filer er oppgitt, bruker wc standard input og viser ikke filnavnene, men bare resultatene.

Eksempler på wc i bruk

La oss se på noen eksempler for å forstå hvordan wc oppfører seg med forskjellige filer. Vi kan begynne med en tom fil:

bash
$ wc tests/inputs/empty.txt
0 0 0 tests/inputs/empty.txt

Som forventet, viser wc at den tomme filen har 0 linjer, 0 ord og 0 byte.

Nå, hvis vi har en fil med en linje tekst, for eksempel "fox.txt", vil wc telle linjer, ord og byte:

bash
$ wc tests/inputs/fox.txt 1 9 48 tests/inputs/fox.txt

Her har vi 1 linje, 9 ord og 48 byte. Når vi ser på innholdet i filen med kommandoen cat -te (som viser tegnene på slutten av linjen og tabulatorer som ^I), ser vi at linjen er:

ruby
The quick brown fox^Ijumps over the lazy dog.$

Merk at tabulatoren (^I) og mellomrommene er med i tellingen, som gjør at byte-tellingen blir 48, men ordtellingen forblir 9, da wc ser på sekvenser av tegn mellom mellomrom og tabulatorer som ord.

Når vi jobber med mer komplekse filer, for eksempel en tekst med Unicode-tegn som i "atlamal.txt", kan wc vise ulik byte- og tegntelling avhengig av hvordan det håndterer Unicode:

bash
$ wc tests/inputs/atlamal.txt 4 29 177 tests/inputs/atlamal.txt

I dette tilfellet har vi 4 linjer, 29 ord og 177 byte. Hvis vi spesifiserer flagget -m, kan wc gi oss tegn i stedet for byte, men dette kan variere avhengig av systemets locale-innstillinger.

Funksjoner som brukes i implementeringen

For å lage et program som etterligner wc, er det flere funksjoner og metoder i Rust som er nyttige:

  1. Iterator::all: Denne funksjonen kan brukes for å iterere gjennom alle elementer i en samling og sjekke en betingelse for hver av dem.

  2. BufRead::read_line: Denne metoden leser en linje fra en fil eller standard input og bevarer linjeskiftet, noe som er viktig for å telle linjer nøyaktig.

  3. take-metoden: Denne metoden kan brukes både på iteratorer og filhåndtak for å begrense antallet elementer som leses. Den kan være nyttig når du ønsker å stoppe tidlig under behandling av store mengder data.

  4. Type-konvertering med as: Rust tilbyr muligheten til å konvertere mellom forskjellige typer med as-nøkkelordet. Dette kan være nødvendig når du konverterer mellom string-representasjoner av tall og faktiske numeriske verdier.

  5. Turbofish-operatoren (::<>): Denne brukes til å spesifisere generiske typer eksplisitt når det er uklart for kompilatoren hva typeparametrene skal være.

Modulering og testing

En viktig del av programmet er å lage en modul for enhetstester. Rust tillater oss å lage tester for å sjekke at programmet vårt oppfører seg riktig. Vi kan også lage en "falsk" filhåndtak for testing slik at vi kan simulere filinnhold uten å måtte opprette fysiske filer hver gang.

Det er også viktig å bruke betinget kompilering for testing. Dette gjør at vi kan inkludere testspesifik kode som bare kompilere når vi kjører testene, og ikke i produksjonsversjonen av programmet.

Videre arbeid med wc

I de neste kapitlene vil vi lære mer om Rusts iteratorer og hvordan vi kan dele opp input i linjer, bytes og tegn. Vi vil også forbedre forståelsen av hvordan wc fungerer, og hvordan vi kan utvide programmet med flere funksjoner, som å støtte forskjellige filformater og internasjonalisering for å håndtere ulike tegnsett.

Hvordan håndtere input-argumenter og filer i Rust-programmer

Når man utvikler Rust-programmer som krever håndtering av filinnganger og argumenter fra kommandolinjen, er det viktig å forstå hvordan man strukturerer disse argumentene for å gjøre programmet fleksibelt og effektivt. I denne sammenhengen skal vi se på hvordan man kan bruke biblioteket clap for å definere og parse argumenter, åpne filer og håndtere ulike innstillinger som påvirker hvordan programmets output blir generert.

Rusts clap-bibliotek gir en praktisk måte å håndtere kommandolinjeargumenter på, og det tillater programmerere å definere hvilke argumenter som er nødvendige, valgfrie og hvordan disse argumentene skal påvirke programmet. I vårt tilfelle jobber vi med et program som tar inn to filer, sammenligner dem og gir et spesifikt utvalg av data basert på brukervalget.

Når du definerer argumentene for et slikt program, starter du med å bruke clap sin Command-struktur. Argumentene som kan defineres inkluderer filstiene, samt forskjellige flagg som styrer hvordan filene skal behandles, for eksempel om kolonner skal vises eller skjules, om sammenligningen skal være sensitiv på store og små bokstaver, og hva som skal være output-delimiteren.

I eksempelet vårt har vi et program som forventer to posisjonelle argumenter: file1 og file2. I tillegg tillater programmet at flere valgfrie argumenter angir om spesifikke kolonner skal vises eller skjules, samt om sammenligningen skal være case-insensitive. Det er også mulig å sette en tilpasset delimiter for output i stedet for den vanlige tabulatoren.

For å sette opp disse argumentene, kan vi bruke følgende kode i clap:

rust
fn get_args() -> Args {
let matches = Command::new("commr")
.
version("0.1.0") .author("Ken Youens-Clark") .about("Rust version of `comm`") .arg(Arg::new("file1").value_name("FILE1").help("Input file 1").required(true))
.arg(Arg::new("file2").value_name("FILE2").help("Input file 2").required(true))
.
arg(Arg::new("suppress_col1").short('1').action(ArgAction::SetTrue).help("Suppress printing of column 1"))
.arg(Arg::new("suppress_col2").short('2').action(ArgAction::SetTrue).help("Suppress printing of column 2"))
.
arg(Arg::new("suppress_col3").short('3').action(ArgAction::SetTrue).help("Suppress printing of column 3"))
.arg(Arg::new("insensitive").short('i').action(ArgAction::SetTrue).help("Case-insensitive comparison of lines"))
.
arg(Arg::new("delimiter").short('d').long("output-delimiter").value_name("DELIM").help("Output delimiter").default_value("\t")) .get_matches(); Args {
file1: matches.get_one("file1").cloned().unwrap(),
file2: matches.
get_one("file2").cloned().unwrap(), show_col1: !matches.get_flag("suppress_col1"), show_col2: !matches.get_flag("suppress_col2"), show_col3: !matches.get_flag("suppress_col3"), insensitive: matches.get_flag("insensitive"),
delimiter: matches.get_one("delimiter").cloned().unwrap(),
} }

Her defineres filene som nødvendige, og de tre kolonnene kan slås av og på ved hjelp av de relevante flaggene. Argumentene for sammenligning av store og små bokstaver, samt delimiteren for output, er også inkludert. Det er viktig at clap sin syntaks er fleksibel og lar oss sette standardverdier for de fleste argumentene, som gjør programmet vårt mer robust og brukervennlig.

Etter at argumentene er definert, er neste steg å åpne filene som er spesifisert av brukeren. Dette kan gjøres med en open-funksjon som håndterer både filstier og standardinput. Hvis en fil ikke kan åpnes, for eksempel fordi den ikke eksisterer, vil programmet kaste en feilmelding:

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).map_err(|e| anyhow!("{filename}: {e}"))?, ))), } }

Denne funksjonen åpner filene, og dersom det er spesifisert en bindestrek ("-"), benyttes standard input (STDIN) i stedet for en fil. Feilmeldinger blir tilpasset for å gjøre det lettere å finne ut hvilken fil som forårsaket feilen. Denne metoden gir en effektiv og trygg måte å håndtere filinnlesing på.

Når filene er åpnet, er neste steg å sammenligne innholdet i dem. Dette krever at vi leser linje for linje, og deretter avgjør hvordan de skal vises i programmet, avhengig av argumentene som ble satt. Hvis begge filene er tomme, vil programmet vise ingenting. Hvis den ene filen er tom, vil bare dataene fra den andre filen vises.

I vårt program sammenligner vi linjer fra to filer og viser hvilke linjer som er unike for hver fil eller som overlapper. En viktig detalj her er at comm-kommandoen (som vårt program etterligner) viser forskjellen på linjer i et bestemt format: først vises linjer som finnes i begge filene, deretter linjer som kun finnes i én av filene. Dette er viktig å forstå for leseren, da det hjelper til med å strukturere utdataene på en logisk måte.

En annen viktig faktor er håndteringen av blanke linjer, som kan føre til at resultatene ser ut som om det er en linje som "mangler". Programmet bør være i stand til å håndtere slike tilfeller uten å krasje eller vise uventede resultater.

Ved å bruke BufRead::lines kan vi enkelt lese linjene i filene, og programmet kan bruke ulike betingelser for å bestemme hvilke linjer som skal vises. Ved sammenligninger av tekstlinjer, spesielt når case-insensitivity er aktivert, bør programmet sikre at store og små bokstaver ikke påvirker resultatet.

Når det gjelder feilbehandling, er det viktig at programmet stopper og gir en klar feilmelding hvis begge filene er satt til å være standard input. Dette kan føre til uventede problemer, og det er avgjørende å håndtere slike tilfeller tidlig i programflyten.

Hvordan lage et program som tilfeldig velger eller filtrerer fortellinger basert på mønstre?

I en verden full av tilfeldigheter og programmering finnes det sjeldne øyeblikk når begge elementene møtes for å skape en uventet, men allikevel fantastisk løsning. Et slikt møte finner sted når vi arbeider med et program som skal lese, sortere og tilfeldig velge fortellinger fra en samling av tekstfiler. I denne sammenhengen er det ikke bare viktig å hente, men også å gi muligheten til å filtrere eller tilfeldig velge historier basert på et gitt mønster eller en forhåndsdefinert frøverdi.

Programmet som skal beskrives her, består av flere komponenter. Først må vi hente og lese inn tekstene fra en samling med filene som ligger på forskjellige steder på systemet vårt. Deretter skal vi kunne filtrere disse tekstene etter et spesifikt mønster, eller bare hente ut en tilfeldig valgt tekst hver gang programmet kjøres. Dette innebærer bruk av flere funksjoner og verktøy som både håndterer filinnlesning, mønstergjenkjenning og tilfeldige valg.

En viktig del av prosessen er funksjonen read_fortunes, som leser inn tekstene fra filene og lagrer dem i et strukturert format. Denne funksjonen skal være i stand til å håndtere flere filer, og den må også kunne ignorere tomme eller ugyldige tekster. Når en tom tekst blir funnet, skal den automatisk bli fjernet. Etter at tekstene er lest inn, er det på tide å sortere dem og forsikre seg om at de er i riktig rekkefølge før videre behandling.

Når tekstene er sortert, kan programmet enten skrive ut alle fortellingene som matcher et spesifikt mønster, eller, i fravær av et mønster, tilfeldig velge en tekst å vise til brukeren. For å gjøre dette på en effektiv måte, implementeres funksjonen pick_fortune. Denne funksjonen tar et array av fortellinger og et valgfritt frø, og bruker en tilfeldig generator for å velge en fortelling basert på frøverdien. Dersom ingen frøverdi er gitt, vil programmet bruke et systemgenerert tilfeldig tall, som sikrer at valget er tilfeldig hver gang programmet kjøres. Når et frø er gitt, blir valget forutsigbart, og hver gang programmet kjøres med det samme frøet, vil det velge den samme fortellingen.

En ekstra utfordring kommer i form av lesing og behandling av data fra filene. Det er viktig å sørge for at programmet håndterer feil på en god måte. For eksempel, dersom en fil er utilgjengelig eller ikke kan åpnes, skal programmet avslutte med en beskrivende feilmelding. Dette kan inkludere feil som rettighetsproblemer (f.eks. "Permission denied") eller at filen ikke finnes. Ved å håndtere slike feil på riktig måte, kan brukeren få en god forståelse av hva som gikk galt, og eventuelt rette opp i problemet.

En annen viktig funksjon som programmet må tilby, er muligheten til å filtrere fortellingene basert på et regulært uttrykk (regex). Dette gjør det mulig for brukeren å spesifisere et mønster, og programmet vil da skrive ut alle de tekstene som passer til dette mønsteret. Denne funksjonen er nyttig hvis brukeren ønsker å finne fortellinger som inneholder bestemte ord eller setninger. Dersom mønsteret ikke matche noen av tekstene, bør programmet informere brukeren om at ingen fortellinger ble funnet som stemmer overens med mønsteret.

I tillegg til de tekniske aspektene ved implementeringen, er det viktig at programmet er brukervennlig. Brukeren bør kunne spesifisere filene som skal leses, enten via kommandolinjen eller ved å bruke argumenter. Det bør også være enkelt å spesifisere et mønster som fortellingene skal matches mot, og å angi et frø dersom brukeren ønsker et forutsigbart tilfeldig valg.

En viktig ting å merke seg i denne typen programmering er at det er svært viktig å forstå hvordan tilfeldighet og mønstergjenkjenning fungerer sammen. For eksempel, dersom programmet skal bruke et tilfeldig frø, er det viktig å sikre at resultatene er reproduserbare. Dette kan være nyttig i tester, ettersom vi kan sikre at programmet alltid velger den samme fortellingen når vi tester det med det samme frøet.

Til slutt, når du implementerer dette programmet, er det essensielt å bruke de riktige verktøyene og bibliotekene for å gjøre oppgavene lettere. Rust, som er språket brukt i eksempelet her, tilbyr et kraftig sett med funksjoner som gjør det enkelt å lese filer, generere tilfeldige tall, håndtere feil og implementere regulære uttrykk. Gjennom grundig testing og implementering av funksjonene kan programmet utvikles til et robust og pålitelig verktøy for å håndtere tekstbaserte fortellinger.

Programmet gir en interessant mulighet til å utforske hvordan man kan bruke tilfeldighet i programmering, samtidig som det lærer oss om håndtering av filer, feil og mønstre. Dette kan utvides til å lage enda mer avanserte applikasjoner som bruker lignende teknikker til å håndtere større datamengder eller mer komplekse mønstre.

Hvordan parser man posisjonslister, felter og bytes i tekstfiler på en robust måte?

Når man arbeider med tekstfiler som inneholder strukturerte data – særlig i formater som CSV eller andre skilletegn-separerte filer – er det ofte behov for å trekke ut spesifikke deler av hver linje: tegn, byte, eller felter. Dette kan virke trivielt, men robust parsing forutsetter at man forstår både formatet og semantikken i dataene. Parsing handler ikke bare om å kutte i en streng – det handler om å gjøre det presist, forutsigbart, og på en måte som overlever ulike inputvariasjoner.

En av de sentrale utfordringene ved parsing av posisjonslister er å validere og tolke argumentene riktig. For eksempel kan brukeren spesifisere at de vil ha ut kolonne 3 til 5, eller tegn 1, 2 og 7. Dette krever en logikk som tolker slike uttrykk som et sett av entydige posisjoner, gjerne representert som en Vec<usize>, og som må valideres for overlapp, sortering og gyldighet.

Når man trekker ut bytes eller tegn, må man ta høyde for at de ikke alltid samsvarer én til én. I UTF-8 kan ett tegn være representert av flere bytes, og å behandle bytes som tegn uten å vite om kodingen, kan føre til korrupsjon av data eller panikk i runtime. Derfor bør man alltid eksplisitt skille mellom bytebasert og tegnbasert ekstraksjon.

Ved parsing av felter i CSV-filer er det nødvendig å ta i bruk en parser som forstår komplekse scenarier – som felt innelukket i anførselstegn, felt som inneholder skilletegnet, eller tomme felter. csv::StringRecord-typen i Rust gir mulighet til å hente ut spesifikke felter ved indeks, og gir samtidig en robust feilmodell dersom indeksen er ute av rekkevidde. Dette gir utvikleren trygghet for at programmet enten oppfører seg korrekt, eller feiler eksplisitt.

Et annet viktig aspekt er hvordan man validerer skilletengnet. I mange verktøy, som for eksempel cutr, må skilletegnet være et enkelt byte (u8), og ikke et flerbyte-tegn som en emoji. Å forsøke å bruke et ugyldig tegn bør derfor fanges opp i en tidlig valideringsfase. Det samme gjelder for spesialtegn som tabulator og komma, som ofte har ulik semantikk i ulike kontekster, og som må oversettes riktig av brukeren eller verktøyet.

Parsing av posisjonslister (som 1,3-5,7) krever en funksjon som kan dele opp inndataen på komma, tolke intervaller, og konstruere en ikke-redundant og sortert liste med unike posisjoner. Feil som overlappende intervaller, negative tall eller ugyldige formater bør gi eksplisitte feilmeldinger. Dette er ikke bare for brukerens skyld, men også for å sikre at nedstrøms behandling av data ikke opererer på feil grunnlag.

Ettersom mange verktøy bruker en lik struktur for kommandolinjeargumenter, gir det mening å bruke makroen clap::Parser med derive-attributter, slik at man kan validere og dokumentere argumentene direkte i koden. Dette skaper både selvdokumenterende kode og eksplisitt validering, som igjen gir færre feil i runtime.

Det er også essensielt å forstå forskjellen mellom felter, bytes og tegn når man bygger verktøy som etterligner Unix-programmer som cut, awk eller grep. Disse har ofte historisk ulik oppførsel, og i et moderne verktøy som f.eks. cutr, må man bestemme seg for hvilke avvik man ønsker å beholde for bakoverkompatibilitet, og hvilke man vil forbedre for bedre robusthet.

Et viktig tillegg for utvikleren er å teste programmets utdata opp mot forventet oppførsel, og å gjøre det via automatiserte integrasjonstester. Dette gjelder særlig for edge-cases, som uleselige filer, manglende input, eller feilplasserte argumenter. Bruk av dies-makroen for forventet panikk i tester, samt verifisering av STDERR, er en god praksis for slike verktøy.

Det er verdt å merke seg at god parsing også handler om hvordan man kommuniserer feil. Et tydelig og spesifikt feilsystem, som for eksempel “Ugyldig intervall: ‘5--3’”, gir brukeren presis informasjon om hva som gikk galt og hvordan det kan fikses.

Å lage slike verktøy handler ikke bare om å trekke ut tekst, men om å forstå hvordan tekst representerer strukturert informasjon, og hvordan den skal behandles presist i nærvær av mange små, men kritiske, detaljer.

Et viktig moment som bør forstås, men som ofte overses, er sk