Kommandot cat, ursprungligen skapat för att sammanfoga och visa innehållet i filer, används idag ofta bara för att visa enskilda filer i terminalen. När cat körs på en tom fil genereras ingen utdata, vilket motsvarar filens innehåll, eller snarare avsaknaden av innehåll. När kommandot körs på filer med textinnehåll visas raderna i filen precis som de är, vilket gör cat till ett enkelt men kraftfullt verktyg för filvisning.

För att göra utskriften mer användbar kan cat numrera rader i utdata med flaggorna -n och -b. Skillnaden är subtil men viktig: -n numrerar alla rader, inklusive tomma, medan -b numrerar endast icke-tomma rader. Det är särskilt tydligt när man hanterar texter med tomma rader emellan, såsom dikter eller stycken i en textfil. Numreringen är högerjusterad i ett fält på sex tecken och följs av en tabulator, vilket gör linjerna lätta att identifiera.

Det är också värt att notera att man kan kombinera -n och -b, men i praktiken prioriterar cat alltid -b när båda används, vilket kan vara förvirrande. I utmaningsprogram ska endast en av dessa flaggor användas åt gången, för att undvika tvetydighet i radnumreringen.

Cat hanterar fel på ett förutsägbart sätt. Om en fil inte existerar eller inte kan öppnas på grund av behörighetsproblem skrivs ett felmeddelande till standardfelströmmen (STDERR), medan programmet fortsätter att bearbeta efterföljande filer. Detta säkerställer att användaren informeras om problem men att processen inte avbryts i onödan.

När cat körs på flera filer i följd, startar BSD-versionen om radnumreringen för varje ny fil, medan GNU-versionen fortsätter numrera över alla filer. Denna skillnad kan påverka hur man tolkar numrering i kombinerade utdata och bör beaktas vid automatiserade tester eller när man skriver kompatibla program som catr.

I utvecklingsarbetet med en liknande applikation i Rust är det rekommenderat att börja med att definiera parametrarna som programmet ska hantera. Här används en struktur (struct) för att representera kommandoradsargumenten, inklusive listan över filnamn och flaggor för radnumrering. Denna metod möjliggör tydlig hantering av input och ger en grund för vidare utveckling.

Testdriven utveckling (TDD) är central i processen att skriva programmet. Genom att först skriva tester och sedan implementera funktionaliteten säkerställs att kraven uppfylls och att koden fungerar som förväntat. Detta sätt att arbeta gör det lättare att refaktorera och förbättra koden utan att introducera nya fel.

Förutom att förstå de tekniska detaljerna i hur cat fungerar, är det viktigt att ha insikt i filsystemets behörigheter och felhantering, då dessa påverkar programmets stabilitet och användarupplevelse. Att kunna skilja på olika versioners beteende (BSD vs GNU) är också värdefullt, särskilt vid utveckling av verktyg som ska vara kompatibla med olika miljöer.

Det är också betydelsefullt att se testning inte bara som ett verktyg för att verifiera funktionalitet, utan som en del av designprocessen som hjälper till att forma programmets arkitektur och gränssnitt från början. Detta leder till mer robust och underhållbar kod.

Slutligen understryks vikten av att arbeta med verkliga testdata och att automatisera tester för att kontinuerligt säkerställa att programmet beter sig som förväntat under olika förhållanden, inklusive hantering av tomma filer, olika textformat och felaktiga filer.

Hur man använder Cargo, Rust och relaterade verktyg effektivt för att skapa kommandoradsprogram

När du börjar arbeta med Rust, en kraftfullt typad och minnessäker programmeringsspråk, kommer du snabbt att stöta på Cargo – ett av de viktigaste verktygen i Rust-ekosystemet. Cargo är en allt-i-ett-lösning för bygg, paket, beroenden och tester i Rust. Alla projekt i denna bok kommer att byggas med hjälp av Cargo, och det rekommenderas att du skapar dina projekt i en egen mapp som till exempel "lösningar". På så sätt kan du enkelt hålla reda på och testa varje kapitel för sig.

För att börja med Cargo, kan du skapa ett nytt projekt genom att köra kommandot cargo new ditt_projekt. När projektet är upprättat, kommer du att använda Cargo för att bygga och testa din kod. För varje kapitel kommer du att kopiera testmappen från bokens repository och använda den för att testa din kod. Ett enkelt sätt att testa om ditt program fungerar som förväntat är att köra cargo test, vilket automatiskt kör alla tester i ditt projekt.

Under arbetet med Rust bör du vara medveten om att vissa program kan bete sig annorlunda beroende på vilket operativsystem du använder. Jag har testat alla program på macOS, Linux, Windows 10/PowerShell och Ubuntu Linux/Windows Subsystem for Linux (WSL). Specifikt två program, findr och lsr, fungerar något olika på Windows jämfört med Unix-liknande system på grund av fundamentala skillnader mellan operativsystemen. Om du använder Windows/PowerShell, rekommenderar jag att du installerar WSL och arbetar i den miljön för att undvika vissa problem.

En annan viktig aspekt av utveckling i Rust är kodens läsbarhet. För att hjälpa till att hålla koden stilren och konsekvent, använder vi verktyget rustfmt. Detta gör att din kod ser snygg ut och är lättare att förstå. Du kan köra cargo fmt för att formatera all källkod i ditt projekt eller konfigurera din kodredigerare för att köra det varje gång du sparar din kod. Personligen använder jag vim, och jag har konfigurerat det för att automatiskt köra rustfmt varje gång jag sparar mitt arbete. Detta gör det mycket lättare att läsa och hitta eventuella misstag.

En annan användbar rekommendation är att installera och använda Clippy, en linter för Rust-kod. Clippy är ett verktyg som analyserar din kod för vanliga misstag och ger förslag på förbättringar. För att installera Clippy, kör rustup component add clippy, och sedan kan du använda kommandot cargo clippy för att få en genomgång av din kod. Om Clippy inte ger några förslag, betyder det att din kod är utan uppenbara problem.

Sedan boken publicerades första gången 2022, har Rust och dess verktyg genomgått flera förändringar. En stor förändring har skett i användningen av clap, ett bibliotek som används för att hantera kommandoradsargument. Versionen av clap som användes i boken när den skrevs var 2.33 och hade enbart en metod för att hantera argument. Den nya versionen 4 introducerade flera nya funktioner, och jag har omarbetat alla exempel för att använda dessa nya funktioner och mönster. Du kan hitta de uppdaterade versionerna av koden i specifika grenar på GitHub. På så sätt kan du följa med i bokens utveckling och testa de senaste versionerna av koden.

För att göra användningen av de här verktygen ännu mer effektiv, tänk på att alltid hålla ditt utvecklingsmiljö uppdaterad. Rust utvecklas snabbt, och nya versioner av både språket och dess verktyg kommer ofta. Genom att hålla koll på de senaste uppdateringarna och förstå de nya funktionerna kan du se till att du använder de bästa metoderna för att skriva robust och effektiv kod.

Det är också viktigt att komma ihåg att när du använder exempel från boken eller annan dokumentation, måste du vara medveten om rättigheterna kring användning av koden. Generellt sett är det okej att använda exempel och kod i dina egna program, men om du planerar att distribuera eller sälja koden, måste du få tillstånd från förlaget. Du får gärna citera exempel och använda dem för att lösa tekniska problem, men kom ihåg att alltid följa de licensregler som gäller för exempel som ingår i böcker och andra resurser.

Att använda Cargo, rustfmt, Clippy och hålla koll på utvecklingens senaste framsteg är viktiga förutsättningar för att bli en skicklig Rust-utvecklare. Rust är ett kraftfullt och snabbt språk, och genom att använda dessa verktyg på rätt sätt, kan du effektivisera din utveckling och skapa hållbara och effektiva program. Det kommer att göra det mycket lättare att skriva kod som inte bara fungerar utan också är stilren, lättläst och fri från vanliga misstag.

Hur kan man på ett effektivt sätt refaktorera och filtrera filsystemdata i Rust med iteratorer?

Kod med många inbäddade villkor och komplexa booleska operationer skapar ofta en svårhanterlig och svårutvecklad kodbas. När man behöver lägga till fler urvalskriterier kan det snabbt bli ohanterligt och risken för fel ökar. Refaktorering, som innebär att omstrukturera koden utan att ändra dess funktionalitet, är ett nödvändigt steg för att göra koden mer läsbar och underhållbar. Viktigt är att ha en fungerande lösning först, där tester säkerställer att refaktorerade förändringar inte bryter existerande funktionalitet.

I Rust är ett effektivt sätt att filtrera och bearbeta filsystemobjekt att utnyttja iteratorer och funktioner som Iterator::filter, vilket gör koden både kompakt och uttrycksfull. Två viktiga koncept är closures (anonyma funktioner) och strömmar av data (iterators). Closures kan fånga variabler från sitt omgivande sammanhang, vilket gör dem perfekta för dynamiska filter baserade på användarens indata.

En första closure kan filtrera filsystemets poster utifrån deras typ — till exempel om en post är en symbolisk länk, en katalog eller en fil. Genom att definiera en enum som beskriver dessa typer och matcha mot den med Rusts match-uttryck, får man en fullständig och säker kontroll över alla möjliga varianter. Om man råkar missa en variant ger kompilatorn ett tydligt felmeddelande, vilket minskar risken för buggar och underlättar framtida utbyggnad.

Den andra closuren används för att filtrera poster baserat på deras namn med hjälp av reguljära uttryck. Genom att använda Iterator::any kan man enkelt kontrollera om ett filnamn matchar någon av de angivna regexarna, och därmed selektera de önskade filerna.

När man använder WalkDir::new(path).into_iter() får man en iterator som returnerar resultatobjekt (Result). Iterator::filter_map är ett kraftfullt verktyg här: man kan hantera fel (skriva ut till standardfel) och samtidigt filtrera bort icke-giltiga poster, och låta giltiga poster passera vidare. Sedan kan man kedja flera filter- och transformeringar, vilket gör att koden blir både läsbar och funktionell.

Slutligen omvandlas varje giltig DirEntry till en strängrepresentation av sökvägen, och samlas i en vektor som skrivs ut. Tack vare denna struktur blir det enkelt att i framtiden lägga till ytterligare filter, exempelvis baserade på filstorlek, ändringstid eller ägarskap. Det är alltså inte bara en fråga om att göra koden snyggare — refaktoreringen skapar en flexibel plattform för framtida funktionalitetsutveckling.

För testning på olika plattformar, såsom Unix och Windows, är det viktigt att förstå skillnader i filsystemets beteende, exempelvis hur symboliska länkar hanteras. Tester måste därför vara anpassade så att de klarar plattformspecifika variationer, vilket kan göras genom att jämföra programmets utdata med förväntade filer som är skräddarsydda för respektive system.

Testfunktioner kan konstrueras för att automatiskt köra programmet med olika argument och jämföra dess output mot förväntade resultatfiler. Genom att läsa in, sortera och jämföra rader kan man försäkra sig om att förändringar i koden inte påverkar funktionaliteten negativt, vilket är centralt när man iterativt förbättrar och utökar koden.

Det är väsentligt att förstå att denna metodik inte bara gör koden mer robust och underhållbar, utan också utnyttjar Rusts styrkor i typkontroll och iteratorabstraktioner för att skapa uttrycksfull och säker programvara. Genom att använda dessa verktyg får utvecklaren en tydlig och utbyggbar struktur som med enkelhet kan anpassas till mer komplexa behov och samtidigt bibehålla hög kodkvalitet.

Hur fungerar Cargo och Rusts teststrategier?

I Rust är Cargo inte bara ett verktyg för att bygga och hantera projekt, utan också för att köra och testa program. När du använder Cargo för att köra ett program, är det vanligt att du först får en hjälpmeddelande som visar tillgängliga kommandon och användning. Detta är ofta känt som "usage statement". När du kör ett program med Cargo och sedan använder kommandot ls, kan du lista innehållet i den aktuella arbetsmappen, vilket vanligtvis kommer att avslöja en ny katalog som kallas target. Denna katalog innehåller byggresultaten, och underkatalogen target/debug lagrar alla byggartefakter.

Efter att ha byggt programmet genom att köra cargo run, kan du köra den skapade binära filen direkt från terminalen, som vanligtvis finns i target/debug/hello. Om du gör detta kommer programmet att skriva ut: Hello, world!. Här ser vi en viktig aspekt: Namnet på den körbara filen är inte automatiskt "main", utan snarare det namn som definieras i filen Cargo.toml, där projektets namn specificeras.

I detta fall är projektets namn "hello", vilket också blir namnet på den körbara filen. Detta är en central egenskap i Cargo: det använder metadata från Cargo.toml för att definiera både programnamn och andra viktiga projektinställningar. Här definieras även Rust-utgåvan som ska användas för att kompilera programmet (i detta fall version 2021). För att förstå varför Rust använder "edition", är det viktigt att veta att varje ny utgåva av Rust kan innebära förändringar som inte är bakåtkompatibla. Utgåvor gör det möjligt att införa förbättringar utan att bryta kompatibiliteten för äldre kod.

Förutom att köra ett program med Cargo och förstå den underliggande strukturen, bör vi också tala om tester i Rust. Rusts testsystem är kraftfullt och hjälper till att säkerställa kodens korrekthet. Tester kan delas in i två huvudkategorier: "inside-out" eller enhetstester, där funktioner i programmet testas isolerat, och "outside-in" eller integrationstester, där hela programmet testas som en användare skulle interagera med det. Här fokuserar vi på integrationstester, vilket är det första steget i att bygga robusta applikationer.

För att skapa ett enkelt test i Rust kan vi skapa en ny katalog för tester, tests, som ligger parallellt med källkodskatalogen src. Sedan kan vi skapa en fil för att testa programmet via kommandoraden, till exempel tests/cli.rs. Här skriver vi en grundläggande testfunktion som testar om en viss funktion returnerar ett förväntat värde, i detta fall att en testassertion alltid ska vara sann:

rust
#[test] fn works() { assert!(true); }

Denna funktion är enkel men användbar för att förstå grundläggande testning i Rust. Genom att köra kommandot cargo test kan vi se om testet lyckas. Om vi ändrar true till false, kommer vi att se att testet misslyckas, vilket ger oss värdefull feedback om kodens stabilitet.

När vi börjar med mer avancerade tester, kan vi skapa testfall där vi faktiskt kör systemkommandon, som att köra ls från terminalen, vilket fungerar på både Unix och Windows. Vi kan utöka vår testfunktion för att verifiera att kommandon faktiskt kan köras utan fel:

rust
use std::process::Command;
#[test] fn runs() { let mut cmd = Command::new("ls"); let res = cmd.output(); assert!(res.is_ok()); }

Den här typen av tester är viktiga för att säkerställa att vår applikation kan interagera med systemet korrekt. Om testet misslyckas, innebär det att vi kan ha ett problem med vårt system eller kod som inte fungerar som förväntat.

Men när vi försöker köra vårt eget program, som vi tidigare nämnde är placerat i target/debug/hello, kan vi upptäcka att det inte går att köra kommandot direkt från terminalen. Detta beror på att den aktuella arbetskatalogen inte är en del av systemets sökväg (PATH). För att kunna köra vår egen binära fil måste vi antingen lägga till den i PATH eller använda en relativ sökväg, som ./target/debug/hello, för att explicit referera till programmet i den aktuella katalogen.

En annan aspekt som vi bör förstå när det gäller projektstruktur är hur Cargo hanterar beroenden. Även om vårt exempelprogram inte använder några externa paket, kommer större projekt att behöva referera till externa "crates" för att utöka funktionaliteten. Dessa beroenden listas i Cargo.toml, och Cargo hanterar installation och versionering av dessa beroenden på ett enkelt och säkert sätt.

För att sammanfatta: när du använder Cargo och Rust, är det inte bara viktigt att kunna bygga och köra programmet utan också att förstå hur Cargo hanterar projektstrukturen och hur tester kan användas för att säkerställa att din kod fungerar som den ska. Testning är en grundläggande del av utvecklingsprocessen som hjälper till att eliminera buggar redan innan de blir ett problem.

Det är också värt att notera att beroenden i Rust, eller "crates", följer en strikt semantisk versionshantering (semver). Det innebär att en uppgradering av major-versionen för en crate kan innebära att det sker bakåtkompatibilitetsproblem, och därför är det viktigt att vara medveten om dessa förändringar när man uppdaterar sina beroenden.

Hur testar man program i Rust med assert_cmd och exitkoder?

När man utvecklar program i Rust är det ofta nödvändigt att inte bara kompilera och köra koden utan också att automatiskt testa att programmet fungerar som det ska. Att bara köra programmet och se att det avslutas utan fel är en början, men för att säkerställa kvaliteten krävs mer detaljerade tester. Ett av de verktyg som förenklar denna process är paketet assert_cmd, som låter oss köra binärer från vårt projekt och kontrollera deras beteende i testmiljön. För att göra skillnader i textutdata tydligare används ofta även pretty_assertions, som förbättrar jämförelsen av strängar i assertioner.

I en Rust-projektstruktur definieras dessa paket som utvecklingsberoenden i Cargo.toml under [dev-dependencies], vilket innebär att de endast laddas vid testning eller benchmarking, inte vid normal körning av programmet. När assert_cmd används kan man skapa ett kommando som kör en binär från den aktuella projektkatalogen, och resultatet kan hanteras med Rusts inbyggda Result-typ. Om den förväntade binären inte finns leder unwrap till panik, vilket i testsammanhang är önskvärt då det indikerar ett problem med byggprocessen.

En grundläggande aspekt av programtestning är att kontrollera programmets avslutningsstatus, så kallad exitkod. Enligt POSIX-standarden signalerar en exitkod 0 att programmet avslutats utan fel, medan värden från 1 till 255 indikerar olika typer av fel. Genom att skapa enkla program som alltid returnerar 0 (true) eller 1 (false) kan vi illustrera detta koncept och testa att vårt system tolkar exitkoder korrekt. I Rust görs detta med funktionen std::process::exit, som explicit kan avsluta programmet med en viss kod. Ett program som inte anropar denna funktion och bara returnerar från main() antas automatiskt ha exitkod 0.

För att säkerställa att programmet inte bara returnerar rätt exitkod utan även producerar förväntad utdata används i assert_cmd funktionen output(), som returnerar både status och standardutdata (STDOUT). Denna data konverteras till en UTF-8-sträng och jämförs med det förväntade resultatet med hjälp av pretty_assertions::assert_eq, vilket ger en mer lättläst och färgkodad skillnadsrapport vid misslyckade tester. Detta är särskilt användbart när skillnader i textutdata kan vara små men avgörande.

Rusts testmotor kör ofta tester parallellt för att utnyttja samtidighet, vilket kan resultera i att testresultaten visas i olika ordningar varje gång. Det är dock möjligt att tvinga tester att köras sekventiellt, vilket kan vara praktiskt vid felsökning.

Att förstå hur exitkoder fungerar och att kunna testa både exitkod och utdata är grundläggande för att skriva robusta kommandoradsprogram i Rust. Det säkerställer att programmet både signalerar korrekt status till operativsystemet och levererar förväntad funktionalitet till användaren.

Viktigt är att inse att Rusts Result-typ är ett centralt verktyg för att hantera operationer som kan lyckas eller misslyckas, och unwrap() används ofta i tester för att snabbt hantera fel genom panik, vilket under utveckling ger tydlig feedback. I produktion bör man dock alltid hantera fel mer försiktigt.

När du skriver tester för kommandoradsprogram är det även bra att tänka på att hantera olika typer av fel och situationer, som t.ex. oförväntad indata, nätverksfel eller filsystemsfel, och att verifiera att programmet reagerar korrekt även i dessa fall. Utöver detta kan det vara värdefullt att testa prestanda och resurshantering i programmet, särskilt om det är tänkt att köras i produktionsmiljöer med stora belastningar.