I Rust kan vi skapa funktioner som returnerar formaterade strängar baserat på vissa inparametrar. Ett exempel på en sådan funktion är format_field, som är användbar för att konditionellt formatera utskrifter beroende på ett booleskt värde. När man arbetar med textutmatning och vill ha kontroll över hur informationen presenteras i terminalen, är det vanligt att behöva formatera data i fasta breddmått, som i till exempel tabeller eller rapporter.

Exemplet nedan visar en funktion som formaterar ett tal och returnerar en sträng med ett specifikt antal tecken, men endast om en viss parameter är satt till true:

rust
fn format_field(value: usize, show: bool) -> String {
if show { format!("{value:>8}") } else { "".to_string() } }

Denna funktion tar två parametrar: ett usize-värde och en boolean show. Om show är true formateras talet som en sträng med 8 tecken på bredd, justerat åt höger. Om show är false, returneras en tom sträng. Användningen av String istället för str är viktig här, eftersom funktionen returnerar en dynamiskt skapad sträng som kan växa i minnet, medan str är en oföränderlig och fast längdsträng.

Funktionen kan användas för att kontrollera formatet på olika typer av data, till exempel för att skriva ut antalet rader, ord, byte eller tecken från en fil.

För att testa denna funktion kan vi skapa enhetstester där vi kollar att formateringen fungerar korrekt både för ensiffriga och tvåsiffriga tal:

rust
#[cfg(test)] mod tests { use super::{format_field}; #[test] fn test_format_field() {
assert_eq!(format_field(1, false), "");
assert_eq!(format_field(3, true), " 3");
assert_eq!(format_field(10, true), " 10");
} }

Här testar vi tre olika scenarier: när show är false och ska returnera en tom sträng, samt när show är true och ska formatera ensiffriga och tvåsiffriga tal korrekt.

När vi använder denna funktion i kontexten av en programapplikation som räknar antalet rader, ord och bytes i en fil, kan vi formatera varje kolumn korrekt:

rust
fn run(mut args: Args) -> Result<()> { let mut total_lines = 0; let mut total_words = 0; let mut total_bytes = 0; let mut total_chars = 0; for filename in &args.files { match open(filename) { Err(err) => eprintln!("{filename}: {err}"), Ok(file) => { let info = count(file)?; println!( "{}{}{}{}{}", format_field(info.num_lines, args.lines), format_field(info.num_words, args.words), format_field(info.num_bytes, args.bytes), format_field(info.num_chars, args.chars), if filename.as_str() == "-" { "".to_string() } else { format!(" {}", filename) } ); total_lines += info.num_lines; total_words += info.num_words; total_bytes += info.num_bytes; total_chars += info.num_chars; } } } if args.files.len() > 1 { println!( "{}{}{}{} total", format_field(total_lines, args.lines), format_field(total_words, args.words), format_field(total_bytes, args.bytes), format_field(total_chars, args.chars) ); } Ok(()) }

I detta exempel samlar vi den totala informationen för alla filer som anges och formaterar utdata på ett enhetligt sätt, där varje kolumn är justerad för att ge ett lättläst resultat. Om det finns mer än en fil, skrivs en sammanfattning av totalsummorna ut.

När vi kör programmet med flera filer, bör vi få en sammanfattning som liknar detta:

bash
$ cargo run -- tests/inputs/*.txt
4 29 177 tests/inputs/atlamal.txt 0 0 0 tests/inputs/empty.txt 1 9 48 tests/inputs/fox.txt 5 38 225 total

Denna teknik kan enkelt anpassas för att ge flexibilitet i hur många och vilka kolumner som ska visas beroende på användarens val, och den gör det möjligt att exkludera vissa kolumner genom att helt enkelt sätta false för respektive värde.

För vidare utveckling kan programmet utökas för att efterlikna GNU-versionen av wc istället för BSD-versionen, vilket kan inkludera ytterligare funktioner som att läsa filnamn från en fil eller visa den längsta raden i en fil. Det är också viktigt att vara medveten om regionala skillnader mellan olika versioner av wc, till exempel när det gäller att räkna ord i filer med olika teckenuppsättningar.

Det är också värt att tänka på hur olika teckenkodningar hanteras. Om man kör programmet på filer med japanska tecken, som i exemplet med spiders.txt, kan det visa sig att antalet ord eller tecken inte stämmer överens med de förväntade resultaten beroende på vilken version av wc som används (BSD eller GNU).

Endtext

Hur säkerställer man att en avgränsare är en enda byte i Rust och hanterar positionlistor för filinmatning?

I programutveckling, särskilt när man arbetar med filinmatning i Rust, är det avgörande att validera att parametrar är korrekta och följer de förväntade formatkraven. Ett exempel är hanteringen av en avgränsare (delimiter) i CSV-filer där avgränsaren måste vara en enda byte, eftersom många bibliotek, som csv-cratet, förutsätter att denna är en u8.

För att säkerställa att avgränsaren är korrekt, används metoden String::as_bytes() för att konvertera strängen till en bytevektor. Om längden på denna vektor är något annat än 1, avvisas inmatningen med ett tydligt felmeddelande som återger den ogiltiga avgränsaren, exempelvis:

rust
let delim_bytes = args.delimiter.as_bytes();
if delim_bytes.len() != 1 { bail!(r#"--delim "{}" must be a single byte"#, args.delimiter); } let delimiter: u8 = *delim_bytes.first().unwrap();

Här används bail! från anyhow för att returnera ett fel som omedelbart avbryter funktionen run. Denna metod är fördelaktig eftersom den inte kräver att man manuellt hanterar fel i hela programflödet, utan låter en snabbt signalera kritiska fel. Viktigt är att använda * för att avreferera från en &u8 till en u8, då kompilatorn annars indikerar typkonflikt.

Förutom valideringen av avgränsaren behövs en robust hantering av vilka delar av en fil man vill extrahera, exempelvis fält, byteintervall eller teckenintervall. Detta representeras med en typalias PositionList som en vektor av Range<usize> där varje Range definierar ett intervall som ska extraheras:

rust
type PositionList = Vec<std::ops::Range<usize>>; #[derive(Debug)] pub enum Extract { Fields(PositionList), Bytes(PositionList), Chars(PositionList), }

Detta möjliggör flexibel och tydlig kod som beskriver exakt vilka delar av data som ska användas. En särskild utmaning är att tillåta användare att ange positioner som en kommaseparerad lista av enskilda tal eller intervall, där varje tal måste vara strikt positivt och intervallen definieras med ett bindestreck.

Valideringen av dessa inmatningar sker i funktionen parse_pos som tar emot en sträng och försöker tolka den som en PositionList. Den måste avvisa:

  • Tomma strängar

  • Värden med noll eller tecken som +

  • Värden med icke-numeriska tecken

  • Intervall där första talet är lika med eller större än andra

Genom att använda omfattande enhetstester säkerställs att alla felaktiga format fångas och att bara korrekta positioner accepteras. Exempel på testfall inkluderar:

rust
assert!(parse_pos("".to_string()).is_err());
assert!(parse_pos("0".to_string()).is_err());
assert!(parse_pos("+1".to_string()).is_err());
assert!(parse_pos("a".to_string()).is_err());
assert!(parse_pos("1-1".to_string()).is_err());
assert!(parse_pos("2-1".to_string()).is_err());
assert!(parse_pos("1".to_string()).is_ok()); assert_eq!(parse_pos("1".to_string()).unwrap(), vec![0..1]);

Denna noggranna kontroll säkerställer att programmet är robust och ger användaren tydlig feedback om hur inmatningen bör se ut.

Det är av vikt att förstå att när man arbetar med indexering i Rust, som är nollbaserad, behöver man ofta justera inmatade värden (t.ex. minska med 1) för att anpassa till interna representationer, något som bör kommuniceras tydligt i dokumentationen och användargränssnittet.

Dessutom är det viktigt att skilja mellan hantering av bytes och tecken eftersom i UTF-8 kan ett tecken bestå av flera bytes, vilket påverkar extraktionen beroende på vad som önskas. Detta kan leda till subtila buggar om inte positionerna tolkas korrekt.

För att sammanfatta är noggrann inmatningsvalidering och tydlig felhantering grundläggande för att skapa pålitliga och användarvänliga verktyg för filhantering. Att använda makron som bail! och omfattande tester bidrar till kodens kvalitet och underhållbarhet.

Hur fungerar grep och hur implementerar man en Rust-version?

I denna kapitel kommer vi att skapa en Rust-version av grep, ett vanligt kommandoradsprogram som söker efter text i filer baserat på ett givet reguljärt uttryck. Som standard tar grep input från standardinmatning (STDIN), men det går också att specificera en eller flera filer eller kataloger att söka i, särskilt om man använder en rekursiv option för att hitta alla filer inom en katalog. Programmet visar normalt upp de rader som matchar mönstret, men det går också att invertera matchningen för att hitta de rader som inte matchar. Ett annat alternativ är att skriva ut antalet träffar istället för själva texten som matchar.

Mönsterjämförelsen är som regel skiftlägeskänslig, men genom att använda en särskild option går det att göra jämförelsen skiftlägesokänslig. Medan den ursprungliga versionen av grep har fler funktioner, kommer vi i detta program att implementera en mer grundläggande version. Genom att skriva detta program kommer du att lära dig om:

  • Användning av ett skiftlägeskänsligt reguljärt uttryck.

  • Variationer i syntaktiska reguljära uttryck.

  • Hur man använder bitvis exklusiv-eller-operator i Rust.

När du söker efter ett mönster, använder grep en reguljär uttrycks-syntax som är flexibel men ändå grundläggande. Det går att göra sökningar på hela eller delar av text, och grep fungerar på samma sätt oavsett om vi arbetar med enkla eller utökade reguljära uttryck (BRE eller ERE). När vi använder grep i programmet kan vi till exempel söka efter en specifik textsträng i filer som fox.txt eller bustle.txt.

För att exemplifiera användningen av grep, överväg följande filer: fox.txt (en fil med en enda rad text), bustle.txt (en dikt med åtta rader text och en blankrad), och nobody.txt (en annan dikt med åtta rader text och en blankrad). Här är hur olika grep-kommandon skulle fungera på dessa filer.

Om vi till exempel söker efter ordet "fox" i den tomma filen empty.txt, kommer grep inte att skriva ut något, eftersom inget i filen matchar mönstret. Däremot, om vi söker med ett tomt mönster (grep "" fox.txt), kommer varje rad i filen att matchas och skrivas ut.

En annan intressant aspekt är när man söker efter specifika ord som "Nobody" i nobody.txt. Eftersom "Nobody" är alltid med stort begynnelsebokstav kommer grep att skriva ut de två rader där ordet förekommer. Men om vi söker efter "nobody" (med små bokstäver), får vi ingen träff. Här kan vi använda flaggan -i (skiftlägesokänslig sökning) för att få grep att ignorera skillnaden mellan stora och små bokstäver.

Det går också att använda andra flaggor för att justera grep:s beteende. Flaggan -v inverts sökningen och returnerar de rader som inte matchar mönstret. Om vi till exempel söker efter "Nobody" i nobody.txt och använder grep -v Nobody, får vi de rader som inte innehåller ordet "Nobody". Flaggan -c ger istället en sammanfattning av antalet matchade rader.

När vi söker i flera filer kommer grep att visa upp vilken fil som varje träff kommer ifrån. Exempelvis, om vi söker efter ordet "The" i alla textfiler, kan vi få följande resultat:

makefile
bustle.txt:The bustle in a house bustle.txt:The morning after death bustle.txt:The sweeping up the heart fox.txt:The quick brown fox jumps over the lazy dog. nobody.txt:Then there's a pair of us!

Det går även att använda grep med rekursiv sökning genom att använda flaggan -r, som söker genom alla filer i en katalog. Om vi till exempel söker efter "The" i hela katalogen kan kommandot se ut så här:

perl
grep -r The .

I detta fall söker grep igenom alla filer i den aktuella katalogen och skriver ut varje träff med filnamnet.

För att implementera en Rust-version av grep, börjar du med att skapa ett nytt Rust-projekt med kommandot cargo new grepr. Du kan sedan modifiera din Cargo.toml för att inkludera de nödvändiga beroenden, som exempelvis regex för reguljära uttryck och walkdir för att hantera rekursiva filsökningar.

För att skapa ett robust program är det viktigt att förstå och korrekt implementera de olika funktionerna och flaggorna som grep tillhandahåller. Det handlar inte bara om att hitta en matchning, utan också om att kunna manipulera och presentera resultaten på ett användbart sätt. Det är också värt att notera att olika plattformar och system kan hantera vissa funktioner olika, särskilt när det gäller fildirektoryer eller kommandoradsflaggor. En djupare förståelse för hur och varför grep fungerar på ett visst sätt ger en bra grund för att bygga en egen version som inte bara efterliknar beteendet utan också kan utökas med nya funktioner.

Hur man implementerar en funktion för att hantera numeriska argument i Rust

Att hantera användarinmatningar, särskilt numeriska argument som kan innehålla både positiva och negativa värden, är en vanlig uppgift inom programmering. I Rust kan detta göras effektivt genom att använda reguljära uttryck eller inbyggda funktioner för att bearbeta och validera dessa inmatningar. I denna sektion kommer vi att diskutera hur man implementerar en funktion som tar emot och bearbetar sådana argument, med fokus på korrekt felhantering och effektiv kodstruktur.

En av de viktigaste funktionerna i detta sammanhang är att kunna tolka numeriska argument som kan ha olika tecken. När programmet körs med argument som specificerar ett antal rader eller byte som ska bearbetas, är det viktigt att programmet korrekt tolkar om dessa värden ska användas från början eller slutet av filen. Ett negativt värde, till exempel, indikerar att programmet ska börja från slutet av filen, medan ett positivt värde anger att vi ska börja från början.

För att skapa denna funktion, kan man använda en metod som först validerar inmatningen och sedan omvandlar den till ett heltal som representerar antalet rader eller byte som ska bearbetas. Nedan ser vi ett exempel på hur en sådan funktion kan implementeras med hjälp av Rusts regex-bibliotek:

rust
fn parse_num(val: String) -> Result<i64, anyhow::Error> {
let num_re = Regex::new(r"^([+-])?(\d+)$").unwrap();
match num_re.captures(&val) { Some(caps) => { let sign = caps.get(1).map_or("-", |m| m.as_str()); let signed_num = format!("{sign}{}", caps.get(2).unwrap().as_str()); if let Ok(num) = signed_num.parse() { if sign == "+" && num == 0 { Ok(PlusZero) } else { Ok(TakeNum(num)) } } else { bail!(val) } } _ => bail!(val), } }

I detta exempel används en reguljär uttryck för att matcha en valfri plus- eller minus-tecken följt av ett heltal. Om inmatningen matchar detta mönster, formateras den och omvandlas till ett heltal. Om tecknet är ett plustecken och värdet är noll, returneras en speciell variant för noll, PlusZero. Annars returneras värdet som en TakeNum variant.

Det är också viktigt att notera hur programmet hanterar felaktiga inmatningar. Om användaren till exempel försöker ange en ogiltig sträng, som "foo", kommer programmet att returnera ett fel:

rust
$ cargo run -- tests/inputs/empty.txt -n foo illegal line count -- foo

Detta är en viktig aspekt när man skriver funktioner som ska användas av andra. Säkerställ alltid att programmet kan hantera ogiltiga argument på ett förutsägbart och användarvänligt sätt.

För att effektivisera koden ytterligare kan reguljära uttryck lagras som statiska värden, vilket gör att de inte behöver kompileras varje gång funktionen anropas. Detta gör koden mer effektiv och förhindrar onödiga beräkningskostnader:

rust
use once_cell::sync::OnceCell;
static NUM_RE: OnceCell<Regex> = OnceCell::new(); fn parse_num(val: String) -> Result<i64, anyhow::Error> { let num_re = NUM_RE.get_or_init(|| Regex::new(r"^([+-])?(\d+)$").unwrap()); match num_re.captures(&val) { Some(caps) => { let sign = caps.get(1).map_or("-", |m| m.as_str()); let signed_num = format!("{sign}{}", caps.get(2).unwrap().as_str());
if let Ok(num) = signed_num.parse() {
if sign == "+" && num == 0 { Ok(PlusZero) } else { Ok(TakeNum(num)) } } else { bail!(val) } } _ => bail!(val), } }

Genom att använda OnceCell kan vi säkerställa att regex-uttrycket endast kompilieras en gång under programmets livslängd.

För de som inte vill använda reguljära uttryck, erbjuder Rust också inbyggda funktioner som gör det möjligt att hantera dessa värden utan externa bibliotek. Här är ett exempel på en enklare implementation utan regex:

rust
fn parse_num(val: String) -> Result<i64, anyhow::Error> {
let signs: &[char] = &['+', '-'];
let res = val .starts_with(signs) .then(|| val.parse()) .unwrap_or_else(|| val.parse().map(i64::wrapping_neg)); match res { Ok(num) => {
if num == 0 && val.starts_with('+') {
Ok(PlusZero) } else { Ok(TakeNum(num)) } } _ => bail!(val), } }

I denna version undersöks om argumentet börjar med ett plustecken eller ett minustecken och tolkas därefter. Detta är ett bra alternativ för dem som inte behöver den fulla kraften hos reguljära uttryck.

En annan viktig aspekt är hur vi hanterar icke-integer värden för de numeriska argumenten. Om användaren matar in ogiltiga tecken, såsom bokstäver, bör dessa felaktiga argument avvisas. Den här koden ser till att detta sker genom att generera ett felmeddelande som informerar användaren om vad som gick fel. Exempel på ogiltiga inmatningar kan vara:

bash
$ cargo run -- tests/inputs/empty.txt -n foo
illegal line count -- foo

Sammanfattningsvis är det viktigt att noggrant kontrollera och validera användarinmatningar, särskilt när det gäller numeriska argument. Genom att använda regex eller inbyggda funktioner kan vi skapa robusta och effektiva lösningar som hanterar både giltiga och ogiltiga värden på ett konsekvent sätt.