Po co w ogóle Rust do aplikacji CLI i czego się spodziewać
Rust w skrócie: szybkość, bezpieczeństwo, brak śmieciarza
Rust łączy trzy cechy, które szczególnie dobrze służą aplikacjom CLI: wysoką wydajność, bezpieczeństwo pamięci i brak runtime’u z garbage collectorem. Kompilator wymusza poprawne zarządzanie własnością danych, więc programy są odporne na klasyczne błędy typu użycie zwolnionej pamięci czy wycieki. Jednocześnie po kompilacji dostajesz pojedynczy plik binarny, który uruchamia się natychmiast, bez dogrywania frameworków ani wirtualnych maszyn.
Dla narzędzi konsolowych oznacza to szybki start, niskie zużycie pamięci i dużą przewidywalność. CLI napisane w Ruście zachowuje się jak typowy program w C lub C++ pod względem wydajności, ale jest znacznie trudniejsze do przypadkowego „wysadzenia” przez drobną pomyłkę w zarządzaniu pamięcią. To szczególnie ważne, jeśli chcesz takie narzędzie rozdać zespołowi lub używać w pipeline’ach CI/CD.
Rust nie ma klasycznego runtime’u – po prostu kompiluje się do natywnego kodu maszynowego. Nie ma tu ukrytej wirtualnej maszyny, która włącza się przy każdym starcie programu. Z perspektywy CLI to duża zaleta: narzędzia działają dobrze zarówno na nowym Macu, jak i na przeciętnym serwerze z Linuksem, bez dodatkowej konfiguracji środowiska.
Dlaczego Rust świetnie nadaje się do narzędzi konsolowych
CLI w Ruście korzystają z bogatego ekosystemu bibliotek zaprojektowanych konkretnie z myślą o pracy w terminalu. Popularne paczki, takie jak clap, structopt (obecnie scalone z clap), indicatif czy anyhow, pozwalają szybko zbudować wygodne narzędzie z kolorowym outputem, paskiem postępu, porządną obsługą błędów i czytelnym systemem komend.
Dodatkowo Rust świetnie ogarnia zadania typowe dla narzędzi programistycznych: przetwarzanie plików, operacje na tekstach, parsowanie JSON/YAML/TOML, pracę z siecią. Dzięki temu wiele projektów open source przenosi pomocnicze skrypty z Pythona czy Basza właśnie do Rusta, gdy liczy się wydajność i stabilność przy dużej skali danych.
W codziennej pracy programisty Rust jako język do CLI daje też przyjemny efekt uboczny: uczysz się solidnego typowania, pracy z błędami i projektowania API, a przy okazji powstaje narzędzie, które naprawdę usprawnia procesy w projekcie.
Przykładowe „prawdziwe” projekty CLI w Rust
Do nauki najlepiej nadają się niewielkie, ale realne narzędzia. Zamiast sztucznego „Hello, World!”, lepiej od razu zbudować coś, co da się faktycznie używać z terminala, np.:
- mini-grep – prosty program wyszukujący linie w pliku zawierające dany fragment tekstu,
- mini-todo – narzędzie do zarządzania listą zadań w pliku JSON/TOML,
- konwerter plików – np. z CSV do JSON lub z Markdown do prostego HTML,
- małe narzędzie do renamowania plików według wzorca,
- CLI do strzelania requestami HTTP do API z zapisaniem odpowiedzi do pliku.
Dobry pierwszy projekt ma jedną cechę: jesteś w stanie go dokończyć w rozsądnym czasie, ale jednocześnie wymaga więcej niż jednej funkcji i jednego pliku. Krótko mówiąc – małe, ale „prawdziwe” narzędzie, a nie demonstracja języka.
Mit vs rzeczywistość: Rust tylko do kernela i embedded?
Popularny mit: Rust nadaje się wyłącznie do systemów operacyjnych, sterowników i wbudowanych urządzeń. Rzeczywistość jest prostsza – duża część projektów w Ruście to zwykłe narzędzia konsolowe, serwisy backendowe, analizatory logów, linijki command-line dla DevOps i SRE. Mnóstwo programistów korzysta z Rusta, żeby zastąpić ciężkie, wolne lub trudne w utrzymaniu skrypty Python/Node czymś szybszym i bardziej przewidywalnym.
Drugi mit: „Rust jest zbyt skomplikowany, żeby zaczynać od CLI, lepiej najpierw książka od A do Z”. W praktyce pierwszy działający program w terminalu, który coś realnego robi, dużo szybciej buduje zrozumienie niż tydzień czytania teorii. Trudniejsze elementy – własność, lifetime’y, typy generyczne – i tak wyjdą w praktyce, ale na początku nie są konieczne, by mieć działające narzędzie.
Jak ustawić oczekiwania na start
Krzywa nauki w Ruście jest ostra głównie przez kompilator i system własności. Początkowo wiele komunikatów błędów będzie wydawać się przytłaczających, ale to właśnie kompilator prowadzi krok po kroku do poprawnego rozwiązania. Z CLI jest o tyle wygodniej, że każde uruchomienie programu natychmiast pokazuje efekt – w terminalu od razu widać, czy flaga działa, czy plik się przetworzył, czy błąd jest sensownie obsłużony.
Dobrze jest przyjąć, że na pierwsze 1–2 małe narzędzia Rusta uczysz się bardziej „przy okazji” niż z podręcznika. Skupiasz się na tym, żeby program się zbudował, testy przeszły, argumenty się sparsowały. Kompilator (i linter w edytorze) powoli nauczy, jakich konstrukcji używać, a jakich unikać. To dużo bardziej praktyczna ścieżka niż nauka „na sucho”.
Instalacja Rust: rustup, kanały i środowisko pracy
Rustup jako menedżer toolchainów – fundament instalacji
rustup to oficjalny menedżer toolchainów Rusta. Zarządza wersjami kompilatora, standardowej biblioteki i narzędzi pomocniczych. Daje możliwość przełączania się między różnymi wersjami (stable, beta, nightly), instalowania dodatkowych komponentów i aktualizacji jednym poleceniem. Kluczowa rada: nie instaluj Rusta z paczek systemowych (apt, yum, brew) jako głównego źródła. Te paczki bywają przestarzałe lub zbudowane w sposób, który utrudnia korzystanie z narzędzi takich jak Rust Analyzer.
Rustup instaluje wszystko lokalnie w katalogu użytkownika (najczęściej ~/.cargo i ~/.rustup). Dzięki temu nie potrzeba uprawnień roota, a aktualizacja nie rozwala innych pakietów w systemie. Dodatkowo możesz mieć na jednym komputerze różne wersje Rusta przypisane do różnych projektów.
Instalacja na Linux, macOS i Windows
Podstawowy sposób instalacji jest wspólny dla większości systemów Unixowych (Linux, macOS). W terminalu wystarczy uruchomić:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Skrypt zapyta o tryb instalacji (domyślny jest zazwyczaj najlepszy) i skonfiguruje ścieżki. Po instalacji trzeba zamknąć i otworzyć terminal, aby nowe zmienne środowiskowe zostały wczytane, lub ręcznie załadować plik konfiguracyjny shella, np.:
source ~/.bashrc
# lub
source ~/.zshrc
Na Windows najlepiej użyć Rustup-init.exe z oficjalnej strony Rusta lub instalatora z Visual Studio Code. Rustup zadba o ścieżki w systemie, a po instalacji komendy rustc i cargo powinny być dostępne w wierszu poleceń (PowerShell, cmd) oraz w terminalu wbudowanym w IDE.
Jeśli po instalacji polecenie cargo zwraca „command not found”, najczęściej oznacza to problem z konfiguracją PATH. Warto wtedy sprawdzić, czy w pliku ~/.bashrc lub ~/.zprofile pojawiły się linie dodające $HOME/.cargo/bin do ścieżki i czy są faktycznie wykonywane przy starcie shella.
Co dokładnie instaluje rustup: rustc, cargo i spółka
Domyślna instalacja rustup dostarcza kilka narzędzi, które pojawią się w terminalu:
- rustc – kompilator Rusta, zwykle uruchamiany pośrednio przez Cargo,
- cargo – menedżer projektu, budowania i zależności,
- rustup – menedżer toolchainów i komponentów,
- rust-std – standardowa biblioteka Rusta dla danej platformy docelowej.
Podstawowa kontrola, czy wszystko działa, to wywołanie:
rustc --version
cargo --version
rustup show
Pierwsze dwa polecenia powinny wypisać wersję z dopiskiem (stable) lub podobnym. rustup show pokaże aktywny toolchain i zainstalowane komponenty, co bywa przydatne, gdy w projekcie trzeba użyć innej wersji niż globalna.
Kanały stable, beta, nightly – rozsądny wybór na start
Rust rozwija się w trzech głównych kanałach wydawniczych:
- stable – domyślny, aktualizowany co 6 tygodni, przeznaczony do codziennej pracy,
- beta – kandydat na kolejne stable, używany do testów przyszłych wersji,
- nightly – kompilator z najnowszymi funkcjami, często zmienny, wymagany przez część eksperymentalnych bibliotek.
Częsty błąd początkujących: „zainstaluję nightly, bo jest najnowszy i nauczę się od razu wszystkiego”. W praktyce nightly do nauki to kiepski pomysł – część funkcji może być niestabilna, niektóre przykłady z dokumentacji nie będą działać tak samo, a dziwne błędy kompilacji potrafią zniechęcić. Na start najlepszy jest kanał stable; jeśli jakaś biblioteka CLI faktycznie wymaga nightly, będzie to jasno napisane w jej dokumentacji.
Przełączanie kanałów jest proste:
rustup default stable
rustup default nightly
Dla konkretnego projektu można też ustawić lokalny toolchain za pomocą pliku rust-toolchain.toml, ale w pierwszej aplikacji CLI zazwyczaj nie jest to potrzebne.
Integracja Rusta z edytorem i IDE
Wygodne środowisko pracy bardzo ułatwia start. Niezależnie od edytora centralnym elementem jest Rust Analyzer – językowy serwer LSP, który zapewnia podpowiedzi, nawigację po kodzie, refaktoryzację i statyczną analizę w locie.
Najczęstsze zestawy:
- VS Code – rozszerzenie „rust-analyzer” z Marketplace, plus ewentualnie „CodeLLDB” do debuggera.
- IntelliJ/CLion – plugin „Rust” od JetBrains; potrafi integrować się z Cargo.
- Neovim/Vim – konfiguracja z LSP (np. nvim-lspconfig) i pluginem dla Rust Analyzer.
Do wygodnej pracy tak naprawdę potrzeba tylko trzech rzeczy: działającego cargo, zainstalowanego Rust Analyzer i poprawnie ustawionego PATH, żeby edytor znalazł narzędzia z ~/.cargo/bin. Reszta (konfiguracja formatowania, lintery, dodatkowe pluginy) to już detale, które można dopracować później.
Pierwszy kontakt z Cargo: tworzenie i uruchamianie projektu
Czym jest Cargo i dlaczego nie używać go „jak make’a”
Cargo to serce ekosystemu Rusta. Łączy w sobie funkcje, które w innych językach bywają rozbite na kilka narzędzi: tworzenie projektu, zarządzanie zależnościami, budowanie, testowanie, uruchamianie, dokumentację i publikację na crates.io. Dla aplikacji CLI oznacza to, że od pierwszego dnia masz spójny sposób pracy, niezależnie od wielkości projektu.
Popularny zły nawyk z innych języków to traktowanie Cargo jak prostego „make’a” do wywoływania kompilatora. Cargo robi dużo więcej: analizuje zależności, dba o cache kompilacji, zarządza profilami dev i release, a także umożliwia używanie workspace’ów przy większych projektach. W przypadku prostego CLI nie trzeba znać wszystkich szczegółów, ale dobrze mieć świadomość, że jest to coś więcej niż tylko nakładka na rustc.
Tworzenie nowego projektu binarnego i biblioteki
Nowy projekt CLI startuje zwykle od polecenia:
cargo new my_cli --bin
Flaga --bin tworzy projekt typu binary crate, którego punktem wejścia jest src/main.rs. To właśnie ten wariant będzie używany do pisania aplikacji CLI. Dla porównania, projekt biblioteczny powstaje przez:
cargo new my_lib --lib
i generuje plik src/lib.rs bez funkcji main. Częsty wzorzec w większych CLI to połączenie obu podejść: funkcje i logika w bibliotece, a minimalny main.rs odpalający kod biblioteczny. Na start wystarczy jednak klasyczny projekt binarny.
Struktura wygenerowanego katalogu projektu
Po cargo new my_cli --bin otrzymasz katalog podobny do tego:
my_cli/
├── Cargo.toml
└── src
└── main.rs
Cargo.toml to plik konfiguracyjny projektu: metadane (nazwa, wersja, edycja), zależności, profile kompilacji. src/main.rs zawiera prosty kod startowy z funkcją fn main(). Dopiero po zbudowaniu pojawi się katalog target/, w którym Cargo trzyma zbudowane artefakty (binarki, bibliotekę, pliki pośrednie).
Wygenerowany main.rs zazwyczaj wygląda tak:
Domyślny main.rs i pierwsze uruchomienie
fn main() {
println!("Hello, world!");
}
To klasyk. Jedna funkcja main, jedno wywołanie println!. Wykrzyknik przy nazwie oznacza makro, nie zwykłą funkcję – na tym etapie wystarczy wiedzieć, że zachowuje się jak „wbudowana instrukcja” do wypisywania tekstu na standardowe wyjście.
Uruchomienie aplikacji:
cd my_cli
cargo run
Za pierwszym razem Cargo zbuduje projekt (cargo build pod spodem), a potem go uruchomi. Efekt:
Compiling my_cli v0.1.0 (/ścieżka/do/my_cli)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/my_cli`
Hello, world!
Mit: „aby budować Rustowe CLI, trzeba za każdym razem ręcznie wywoływać rustc”. Rzeczywistość: w codziennej pracy prawie nigdy nie wzywa się rustc bezpośrednio. Wystarcza cargo run, cargo build i tyle.
Różnica między cargo run a cargo build
W prostym projekcie pokusa jest jedna: zawsze odpalać cargo run. Działa, więc po co kombinować? W praktyce warto rozdzielić dwa kroki:
cargo build– kompiluje projekt, ale nie uruchamia binarki,cargo run– kompiluje (jeśli trzeba) i od razu odpala.
Przy aplikacjach CLI, które długo działają lub przyjmują wiele parametrów, wygodniej jest budować raz, a potem uruchamiać gotowe target/debug/my_cli z różnymi argumentami. Dodatkowo:
cargo build --release
tworzy zoptymalizowaną binarkę w target/release/. Ta wersja uruchamia się szybciej i zajmuje mniej miejsca, kosztem dłuższego czasu kompilacji. Przy rozwijaniu pierwszego CLI nie trzeba jej nadużywać – lepiej zostać przy profilu dev, a --release używać, gdy program trafia na produkcyjną maszynę.
Argumenty wiersza poleceń – od std::env::args do małego parsera
CLI bez argumentów to tylko pół-CLI. Podstawowy sposób na ich odczytanie:
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("Odebrane argumenty: {:?}", args);
}
Jeśli uruchomisz:
cargo run -- foo bar 123
to Rust zobaczy coś takiego:
["target/debug/my_cli", "foo", "bar", "123"]
Pierwszy element to ścieżka do binarki, reszta to faktyczne argumenty użytkownika. Prosta wersja programu, który przyjmuje jedno słowo i wypisuje powitanie:
use std::env;
fn main() {
let mut args = env::args().skip(1); // pomijamy nazwę programu
let name = args.next().unwrap_or_else(|| "nieznajomy".to_string());
println!("Cześć, {}!", name);
}
Mit: „od razu trzeba użyć dużej biblioteki do parsowania argumentów, bo inaczej program będzie ‘nieprofesjonalny’”. Rzeczywistość: na start zwykłe env::args wystarcza. Rozbudowaną bibliotekę warto dołożyć wtedy, gdy zaczynają się pojawiać flagi, subkomendy i pomoc.
Pierwsza wersja sensownego CLI: mini-kalkulator
Dobry przykład na rozgrzewkę to prosty kalkulator na dwa argumenty. Założenie: użytkownik podaje operację i dwie liczby całkowite:
my_cli add 2 3
my_cli sub 10 4
Implementacja w src/main.rs:
use std::env;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
eprintln!("Użycie: {} <add|sub> <a> <b>", args[0]);
process::exit(1);
}
let op = &args[1];
let a: i32 = match args[2].parse() {
Ok(n) => n,
Err(_) => {
eprintln!("Błąd: '{}' nie jest poprawną liczbą całkowitą", args[2]);
process::exit(1);
}
};
let b: i32 = match args[3].parse() {
Ok(n) => n,
Err(_) => {
eprintln!("Błąd: '{}' nie jest poprawną liczbą całkowitą", args[3]);
process::exit(1);
}
};
let result = match op.as_str() {
"add" => a + b,
"sub" => a - b,
_ => {
eprintln!("Nieznana operacja: '{}'. Użyj 'add' albo 'sub'.", op);
process::exit(1);
}
};
println!("Wynik: {}", result);
}
Tu pojawia się kilka ważnych wzorców:
- komunikaty o błędach idą na
stderrprzezeprintln!, - błędy wejścia kończą program z niezerowym kodem wyjścia (
process::exit(1)), - parsowanie tekstu na liczby odbywa się przez
String::parse()imatch.
To nadal mały program, ale pokazuje, jak Rust zachęca do jawnego obsługiwania błędów zamiast udawania, że ich nie ma.

Dodawanie zależności: crates.io, Cargo.toml i wersjonowanie
Jak działa ekosystem paczek w Ruście
Oficjalny rejestr paczek to crates.io. Odpowiednik npm dla Node, PyPI dla Pythona czy Maven Central dla Javy. Paczki nazywane są „crate’ami” i mogą być binarne (aplikacje) albo biblioteki. Twój projekt CLI to też crate.
Konfiguracja zależności siedzi w Cargo.toml. Fragment wygenerowanego pliku może wyglądać tak:
[package]
name = "my_cli"
version = "0.1.0"
edition = "2021"
[dependencies]
Sekcja [dependencies] jest pusta – na razie używasz tylko standardowej biblioteki. Jeśli chcesz dorzucić zewnętrzny crate, robisz to tu. Przykład: popularna biblioteka do kolorowania wyjścia terminalowego colored:
[dependencies]
colored = "2"
Następnie:
cargo build
Cargo pobierze colored z crates.io, zbuduje i doda do projektu. W kodzie możesz go użyć tak:
use colored::Colorize;
fn main() {
println!("{}", "Sukces".green());
eprintln!("{}", "Błąd".red().bold());
}
Mit: „Rust wymaga podawania precyzyjnej wersji jak 2.1.4, inaczej wszystko się rozsypie”. Rzeczywistość: wpis "2" oznacza „kompatybilną wersję 2.x.y” zgodnie z semverem. Zazwyczaj to w zupełności wystarcza; drobiazgowe blokowanie wersji zostawia się na bardziej złożone projekty.
Użycie cargo add do wygodnej pracy z zależnościami
Zamiast ręcznie edytować Cargo.toml, wygodniej posłużyć się narzędziem cargo-edit, które dodaje komendę cargo add. Instalacja:
cargo install cargo-edit
Potem:
cargo add colored
i zależność pojawi się automatycznie w sekcji [dependencies]. Przy pracy z małymi CLI to drobna oszczędność czasu, ale przy wielu paczkach bardzo upraszcza życie.
Pierwsza „prawdziwa” biblioteka CLI: clap
Ręczne parsowanie argumentów szybko robi się męczące. Gdy tylko pojawi się potrzeba obsługi flag (--verbose, -h, itp.), podkomend (git commit, git status) i automatycznej pomocy, naturalnym wyborem jest clap.
Dodanie do projektu:
cargo add clap --features derive
W main.rs można teraz napisać:
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "my_cli")]
#[command(about = "Mały przykład CLI w Ruście", long_about = None)]
struct Cli {
/// Operacja: add lub sub
op: String,
/// Pierwsza liczba
a: i32,
/// Druga liczba
b: i32,
}
fn main() {
let cli = Cli::parse();
let result = match cli.op.as_str() {
"add" => cli.a + cli.b,
"sub" => cli.a - cli.b,
_ => {
eprintln!("Nieznana operacja: '{}'", cli.op);
std::process::exit(1);
}
};
println!("Wynik: {}", result);
}
Teraz komenda:
cargo run -- add 2 5
zachowa się jak wcześniej, ale gratis dostajesz:
- automatycznie wygenerowaną pomoc:
cargo run -- --help, - wymuszenie wymaganych argumentów – brakujące parametry generują czytelny komunikat.
Mit: „biblioteki CLI w Ruście są ciężkie i skomplikowane”. Rzeczywistość: dobrze zrobione crate’y typu clap upraszczają 90% powtarzalnego kodu i pozwalają skupić się na logice programu.
Organizacja kodu: od jednego pliku do modułów
Dlaczego warto rozbijać main.rs na mniejsze części
Niewielkie CLI spokojnie mieści się w jednym pliku. Jednak już przy kilku komendach i bardziej rozbudowanej logice main.rs zaczyna puchnąć. Rust zachęca do wydzielania kodu do modułów lub nawet osobnego crate’a-biblioteki.
Prosty sposób na uporządkowanie kodu:
src/main.rs– tylko start programu, obsługa argumentów, wywołanie logiki,src/cli.rs– definicja struktury argumentów (np. zclap),src/logic.rs– właściwa logika biznesowa.
Struktura katalogu:
src/
├── main.rs
├── cli.rs
└── logic.rs
Zawartość src/cli.rs:
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "my_cli")]
#[command(about = "Mały przykład CLI w Ruście", long_about = None)]
pub struct Cli {
/// Operacja: add lub sub
pub op: String,
/// Pierwsza liczba
pub a: i32,
/// Druga liczba
pub b: i32,
}
Zawartość src/logic.rs:
pub fn calculate(op: &str, a: i32, b: i32) -> Result<i32, String> {
match op {
"add" => Ok(a + b),
"sub" => Ok(a - b),
_ => Err(format!("Nieznana operacja: '{}'", op)),
}
}
Zmodyfikowany src/main.rs:
mod cli;
mod logic;
use clap::Parser;
fn main() {
let args = cli::Cli::parse();
match logic::calculate(&args.op, args.a, args.b) {
Ok(result) => println!("Wynik: {}", result),
Err(err) => {
eprintln!("Błąd: {}", err);
std::process::exit(1);
}
}
}
Taki podział od razu ułatwia testowanie. Logika nie zależy od tego, w jaki sposób użytkownik podał dane (argumenty CLI, plik, stdin), więc można ją bez problemu testować jednostkowo.
Wyodrębnienie logiki do biblioteki: src/lib.rs
Kolejny krok to przeniesienie logiki do biblioteki i traktowanie aplikacji CLI jako cienkiej nakładki. To wzorzec często spotykany w poważniejszych projektach: jedna biblioteka, kilku różnych „frontów” (CLI, serwer HTTP, skrypt migracyjny).
Dodaj plik src/lib.rs:
pub mod logic;
pub use logic::calculate;
Przenieś logic.rs do modułu bibliotecznego:
src/
├── main.rs
└── lib.rs
Zawartość nowego src/lib.rs:
pub fn calculate(op: &str, a: i32, b: i32) -> Result<i32, String> {
match op {
"add" => Ok(a + b),
"sub" => Ok(a - b),
_ => Err(format!("Nieznana operacja: '{}'", op)),
}
}
A src/main.rs może wyglądać tak:
mod cli;
use clap::Parser;
use my_cli::calculate; // nazwa crate'a jak w Cargo.toml
fn main() {
let args = cli::Cli::parse();
match calculate(&args.op, args.a, args.b) {
Ok(result) => println!("Wynik: {}", result),
Err(err) => {
eprintln!("Błąd: {}", err);
std::process::exit(1);
}
}
}
Tutaj drobna pułapka: nazwa crate’a w imporcie (use my_cli::...) musi odpowiadać polu name z Cargo.toml. Jeśli projekt nazywa się inaczej, import trzeba dopasować.
Testowanie aplikacji CLI w Ruście
Testy jednostkowe logiki biznesowej
Rozdzielenie logiki od warstwy CLI od razu otwiera drzwi do sensownych testów. Funkcja calculate jest czysta: dla tych samych argumentów zwraca ten sam wynik i nie dotyka I/O. Idealny kandydat do testów jednostkowych.
Do pliku src/lib.rs można dodać prosty moduł testów:
pub fn calculate(op: &str, a: i32, b: i32) -> Result<i32, String> {
match op {
"add" => Ok(a + b),
"sub" => Ok(a - b),
_ => Err(format!("Nieznana operacja: '{}'", op)),
}
}
#[cfg(test)]
mod tests {
use super::calculate;
#[test]
fn add_two_numbers() {
let result = calculate("add", 2, 3).unwrap();
assert_eq!(result, 5);
}
#[test]
fn sub_two_numbers() {
let result = calculate("sub", 10, 4).unwrap();
assert_eq!(result, 6);
}
#[test]
fn unknown_operation_returns_error() {
let err = calculate("mul", 2, 3).unwrap_err();
assert!(err.contains("Nieznana operacja"));
}
}
Uruchomienie testów:
cargo test
Mit: „testy w Ruście są trudne przez zbyt restrykcyjny system typów”. Rzeczywistość: jeśli kod jest zaprojektowany z myślą o czystych funkcjach i minimalnej ilości efektów ubocznych, testy pisze się spokojnie szybciej niż w wielu dynamicznych językach.
Testowanie zachowania CLI z użyciem assert_cmd
Same testy logiki to jedno, ale dobrze mieć też pewność, że aplikacja zachowuje się poprawnie z perspektywy użytkownika: wypisuje odpowiednie komunikaty, ustawia kody wyjścia, obsługuje błędne argumenty.
Przydaje się do tego crate assert_cmd. Dodanie zależności (do sekcji [dev-dependencies], bo jest używana tylko w testach):
[dev-dependencies]
assert_cmd = "2"
predicates = "3"
Rust pozwala mieć testy integracyjne w katalogu tests/. Tworzymy plik tests/cli.rs:
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn add_two_numbers_cli() {
let mut cmd = Command::cargo_bin("my_cli").unwrap();
cmd.arg("add")
.arg("2")
.arg("3")
.assert()
.success()
.stdout("Wynik: 5n");
}
#[test]
fn unknown_operation_cli() {
let mut cmd = Command::cargo_bin("my_cli").unwrap();
cmd.arg("mul")
.arg("2")
.arg("3")
.assert()
.failure()
.stderr(predicate::str::contains("Nieznana operacja"));
}
Command::cargo_bin("my_cli") używa nazwy binarki z Cargo.toml (pola name). Komenda:
cargo test --test cli
zbuduje binarkę i odpali testy integracyjne. To wygodny sposób na weryfikację, że cała ścieżka – od parsowania argumentów po wyjście na ekran – działa zgodnie z oczekiwaniami.
Testy modułów a publiczne API
Gdy logika ląduje w lib.rs, trzeba zdecydować, co ma być publiczne. Częsty błąd na początku: znacznik pub ląduje na wszystkim „na wszelki wypadek”. Dużo rozsądniej jest wystawiać tylko to, czego naprawdę potrzebują inne moduły lub zewnętrzni użytkownicy biblioteki.
Przykład prostego API:
mod core_logic {
pub fn calculate(op: &str, a: i32, b: i32) -> Result<i32, String> {
match op {
"add" => Ok(a + b),
"sub" => Ok(a - b),
_ => Err(format!("Nieznana operacja: '{}'", op)),
}
}
#[cfg(test)]
mod tests {
use super::calculate;
#[test]
fn add_works() {
assert_eq!(calculate("add", 1, 2).unwrap(), 3);
}
}
}
pub use core_logic::calculate;
Moduł core_logic jest wewnętrzny, a na zewnątrz wystawiona jest tylko funkcja calculate przez pub use. Jeśli kiedyś implementacja się zmieni (np. dojdą nowe typy operacji), testy wewnętrzne zabezpieczą szczegóły, ale API pozostanie stabilne.

Konfiguracja i zarządzanie środowiskami
Profile kompilacji: debug vs release
Domyślnie cargo build kompiluje w profilu debugowym: wolniej działa, ale szybciej się buduje i zawiera dodatkowe informacje do debugowania. Do codziennego developmentu to w zupełności wystarczy.
Do dystrybucji lub testów wydajności używa się profilu release:
cargo build --release
Binarka ląduje wtedy w target/release/, a nie w target/debug/.
Konfiguracja profili w Cargo.toml wygląda na przykład tak:
[profile.dev]
opt-level = 0
debug = true
overflow-checks = true
[profile.release]
opt-level = 3
debug = false
lto = true
codegen-units = 1
Dla małego CLI różnice nie będą spektakularne, ale przy bardziej rozbudowanych narzędziach, które np. przetwarzają większe pliki, przejście na profil release potrafi skrócić czas działania wielokrotnie.
Zmienne środowiskowe i konfiguracja z pliku
Nawet proste CLI często potrzebuje konfiguracji: ścieżki do plików, tokeny API, przełączniki trybu debug. Można to opierać tylko na argumentach, ale wygodnie jest łączyć je z plikiem konfiguracyjnym lub zmiennymi środowiskowymi.
Najprościej – użyć standardowej biblioteki:
use std::env;
fn main() {
let debug = env::var("MY_CLI_DEBUG").is_ok();
if debug {
eprintln!("Tryb debug włączony przez zmienną środowiskową");
}
// reszta programu...
}
Gdy konfiguracja rośnie, przydają się dedykowane crate’y, np. config czy dotenvy. Przykład ze wczytywaniem pliku .env w katalogu projektu:
cargo add dotenvy
use dotenvy::dotenv;
use std::env;
fn main() {
dotenv().ok(); // załaduje zmienne z pliku .env, jeśli istnieje
let api_key = env::var("MY_CLI_API_KEY")
.expect("Brak zmiennej MY_CLI_API_KEY");
println!("Ładuję dane z API z użyciem klucza: {}", api_key);
}
Mit: „Rust nie nadaje się na narzędzia, które trzeba będzie konfigurować na wielu środowiskach, bo zarządzanie konfiguracją jest toporne”. Rzeczywistość: dzięki crate’om z ekosystemu możesz mieć obsługę YAML/JSON/TOML, zmiennych środowiskowych i nadpisywania opcji przez CLI bez pisania własnej infrastruktury.
Łączenie konfiguracji z argumentami CLI
Praktyczne podejście: wartości domyślne trzymane są w konfiguracji (plik lub env), a argumenty linii poleceń mogą je nadpisać. clap oferuje tu sporo wygodnych mechanizmów.
Przykład prostego CLI, gdzie adres serwera można podać przez --server albo zmienną MY_CLI_SERVER:
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "my_cli")]
struct Cli {
/// Adres serwera
#[arg(
long,
env = "MY_CLI_SERVER",
default_value = "https://api.example.com"
)]
server: String,
}
fn main() {
let args = Cli::parse();
println!("Używam serwera: {}", args.server);
}
Kolejność priorytetów jest wtedy jasna:
- jeśli użytkownik poda
--server, wygrywa argument, - jeśli nie – brana jest zmienna środowiskowa
MY_CLI_SERVER, - jeśli jej brak – użyta zostaje wartość domyślna z atrybutu
default_value.
Budowanie, wersjonowanie i dystrybucja binarek
Ręczne budowanie binarek na różne platformy
Najprostszy scenariusz: budujesz na tej samej platformie, na której będziesz używać narzędzia. Dla systemu Linux x86_64 wystarczy:
cargo build --release
cp target/release/my_cli /usr/local/bin/
Jeśli narzędzie ma być używane na różnych systemach (np. Linux i Windows), można kompilować na każdej platformie lokalnie lub skorzystać z cross-kompilacji. Dla częstych celów działa to bez większych ceregieli, bo Rust ma gotowe „targety”. Lista dostępnych:
rustup target list
Dodanie nowego celu, np. Windows 64-bit (MSVC):
rustup target add x86_64-pc-windows-gnu
Następnie:
cargo build --release --target x86_64-pc-windows-gnu
W katalogu target/x86_64-pc-windows-gnu/release/ pojawi się binarka dla danego systemu. W przypadku bardziej egzotycznych celów (np. ARM, wbudowane systemy) dochodzi temat zewnętrznych toolchainów, ale dla popularnych platform desktopowych Rust załatwia większość roboty.
Automatyzacja wydawnicza z cargo-dist lub GitHub Actions
Gdy CLI zaczyna mieć użytkowników, ręczne budowanie binarek szybko staje się uciążliwe. Warto wtedy zautomatyzować proces wydawniczy, np. generowanie paczek dla Linux/macOS/Windows na każde wydanie w repozytorium.
Jednym z nowszych narzędzi jest cargo-dist, które generuje skonfigurowane workflow dla GitHub Actions oraz gotowe artefakty (archiwa, instalatory). Instalacja:
cargo install cargo-dist
Inicjalizacja w projekcie:
cargo dist init
Narzędzie doda odpowiednie wpisy do Cargo.toml i pliki workflow w .github/workflows/. Potem wystarczy zrobić tag w repozytorium:
git tag v0.1.0
git push --tags
a GitHub Actions zbuduje binarki, spakuje je i podłączy do wydania. Zamiast ręcznie wrzucać pliki, utrzymuje się prosty proces: commit → tag → automatyczny build.
Wersjonowanie zgodne z semver
W polu version w Cargo.toml typowy format to MAJOR.MINOR.PATCH, zgodny z semverem:
- MAJOR – gdy wprowadzasz zmiany niekompatybilne wstecz,
- MINOR – gdy dodajesz nowe funkcje zachowując kompatybilność,
- PATCH – gdy poprawiasz błędy bez zmiany API.
Mit w świecie Rusta: „do prywatnego CLI wersjonowanie jest nieistotne, więc można zawsze trzymać się 0.1.0”. Rzeczywistość: dopóki z narzędzia korzystasz tylko sam, rzeczywiście niewiele to zmienia, ale w momencie gdy inne osoby integrują się z twoim narzędziem (np. w pipeline’ach CI), przewidywalne podbijanie wersji mocno ułatwia aktualizacje.
Jakość kodu: formatowanie, lintowanie i dokumentacja
Formatowanie z rustfmt
Rust ma oficjalny formatator rustfmt. Jest instalowany razem z toolchainem (jeśli nie – można dodać go poleceniem rustup component add rustfmt). Wywołanie na projekcie:
cargo fmt
Ujednolicone formatowanie ma w Ruście dodatkową zaletę: większość przykładów w ekosystemie wygląda podobnie, więc mniej czasu traci się na „odgadywanie stylu”.
Lintowanie z clippy
clippy to zestaw lintów, które pomagają wychwycić podejrzane konstrukcje, nieefektywny kod lub wzorce niezgodne z idiomami języka. Instalacja komponentu:
rustup component add clippy
Uruchomienie na projekcie:
cargo clippy
Przykładowe ostrzeżenie: użycie expect("coś") tam, gdzie lepiej było zwrócić błąd, albo niepotrzebny klon .clone() na typie kopiowalnym. W małych CLI często pojawia się pokusa, żeby zignorować linty „bo to tylko skrypt”. Lepiej jednak traktować je jako tani kod review – większość sugestii ma konkretny powód.
Podstawowa dokumentacja i cargo doc
Nawet dla małego narzędzia opłaca się zadbać o minimalną dokumentację publicznych funkcji. Rust używa komentarzy w stylu Markdown poprzedzonych trzema ukośnikami:
/// Oblicza wynik operacji `op` na liczbach `a` i `b`.
///
/// Obsługiwane operacje:
/// - `"add"` – dodawanie
/// - `"sub"` – odejmowanie
///
/// Zwraca `Err`, jeśli operacja jest nieznana.
pub fn calculate(op: &str, a: i32, b: i32) -> Result<i32, String> {
// ...
}
Generowanie dokumentacji HTML:
cargo doc --open
Pokaże się lokalnie wygenerowana strona z opisem API. Przydatne nawet w jednoosobowych projektach – po kilku miesiącach łatwiej wrócić do kodu, który ma choćby zwięzłe komentarze w tym formacie.
Najczęściej zadawane pytania (FAQ)
Czy Rust to dobry wybór do tworzenia prostych aplikacji CLI?
Tak, Rust świetnie nadaje się nawet do bardzo prostych narzędzi konsolowych. Daje szybki start programu, niskie zużycie pamięci i pojedynczy plik binarny, który łatwo skopiować na inne maszyny bez instalowania dodatkowego runtime’u czy frameworków.
Mit jest taki, że Rust „opłaca się” dopiero przy dużych, skomplikowanych systemach. W praktyce mały konwerter plików, mini-grep czy narzędzie do rename’owania plików zyskują od razu: są szybkie, przewidywalne i dużo trudniej je wysadzić błędem w pamięci niż odpowiednik w C.
Jak zainstalować Rust i Cargo na Linuxie lub macOS?
Najprościej użyć oficjalnego rustup, uruchamiając w terminalu:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Instalator przeprowadzi przez wybór trybu (zwykle wystarczy domyślny) i doda katalog $HOME/.cargo/bin do ścieżki. Po zakończeniu trzeba otworzyć nowy terminal albo załadować konfigurację shella poleceniem source ~/.bashrc lub source ~/.zshrc.
Mit: „na macOS najlepiej instalować Rust przez brew”. Rzeczywistość: brew bywa spóźnione z wersjami i gorzej współpracuje z narzędziami deweloperskimi; rustup to oficjalny, wspierany sposób i lepiej trzymać się właśnie jego.
Co zrobić, gdy po instalacji Rust cargo nie działa i pojawia się „command not found”?
To zwykle problem ze zmienną środowiskową PATH. Najpierw sprawdź, czy istnieje katalog $HOME/.cargo/bin oraz czy w plikach startowych shella (np. ~/.bashrc, ~/.zshrc, ~/.profile) jest linia dodająca go do PATH, np. export PATH="$HOME/.cargo/bin:$PATH".
Jeśli taka linia jest, upewnij się, że dany plik rzeczywiście się wykonuje przy starcie shella. Częsty przypadek: konfiguracja jest w ~/.profile, ale użytkownik ładuje tylko ~/.zshrc. Wtedy trzeba albo przenieść konfigurację, albo jawnie zrobić source ~/.profile. Po naprawieniu PATH nowe okno terminala powinno widzieć komendy cargo i rustc.
Od czego zacząć: książka o Ruście czy od razu pierwsze narzędzie CLI?
Najpraktyczniej jest od razu napisać małe, ale „prawdziwe” narzędzie CLI i na jego bazie odkrywać język. To może być mini-grep, prosty TODO w pliku JSON czy konwerter CSV → JSON. Kluczowe, żeby projekt dało się spokojnie skończyć w kilka wieczorów.
Popularne przekonanie brzmi: „najpierw trzeba przeczytać Rust Book od deski do deski”. W rzeczywistości pierwszy działający program, który coś realnie robi w terminalu, dużo szybciej oswaja z kompilatorem, błędami i narzędziami. Trudniejsze tematy, jak własność i lifetime’y, naturalnie wypłyną w trakcie pracy zamiast na sucho z teorii.
Jakie są przykładowe projekty CLI w Rust dobre na start?
Dobrymi pierwszymi projektami są między innymi:
- mini-grep – wyszukiwanie linii w pliku zawierających podany fragment tekstu,
- mini-todo – obsługa listy zadań trzymanej w pliku JSON lub TOML,
- konwerter plików – np. CSV → JSON albo Markdown → prosty HTML,
- narzędzie do masowego zmieniania nazw plików według wzorca,
- CLI do wysyłania zapytań HTTP do API z zapisem odpowiedzi do pliku.
Wspólny mianownik: projekt wymaga kilku plików, prostego modelu danych i obsługi błędów, ale nie tonie w złożoności. Dzięki temu uczysz się od razu pracy z plikami, parsowania argumentów i używania popularnych crate’ów zamiast tylko wypisywać „Hello, world!”.
Czym różni się Rust od Pythona czy Node przy tworzeniu narzędzi konsolowych?
Rust kompiluje się do natywnego binarium bez runtime’u z garbage collectorem. Program startuje bardzo szybko, zużywa mało pamięci i nie wymaga instalowania interpretera ani node_modules na maszynie docelowej. W kontekście CLI oznacza to narzędzia przewidywalne, dobrze działające nawet na słabszych serwerach.
Python i Node wygrywają na samym początku prostotą składni, ale w miarę rośnięcia danych i wymagań wydajnościowych zaczynają ciążyć – szczególnie w pipeline’ach CI/CD czy przy przetwarzaniu dużych plików. Rust celuje dokładnie w tę lukę: daje ergonomię nowoczesnego języka, a wydajność i model pamięci bliższe C/C++, tylko z kompilatorem pilnującym typowych min.
Czy Rust naprawdę jest aż tak trudny, żeby zaczynać od czegoś prostszego niż CLI?
Ostrość krzywej nauki w Ruście bierze się głównie z systemu własności i dokładnego kompilatora. Na początku komunikaty błędów mogą przytłaczać, ale jednocześnie bardzo precyzyjnie prowadzą za rękę do poprawnego rozwiązania. W przypadku CLI od razu widać efekty: czy flaga działa, czy plik się przetworzył, czy błąd jest dobrze opisany.
Mit: „CLI w Ruście to zabawa dla zaawansowanych”. Rzeczywistość: pierwszy działający program konsolowy to jedno z lepszych miejsc na start, bo cykl: uruchom – zobacz – popraw – powtórz jest ekstremalnie krótki. Do prostego narzędzia nie trzeba od razu rozumieć lifetime’ów czy generyków, a kompilator i linter w edytorze sukcesywnie podnoszą poprzeczkę jakości kodu.
Bibliografia i źródła
- The Rust Programming Language. No Starch Press (2019) – Podstawy Rusta, własność, błędy, przykłady CLI (minigrep).
- Rust Reference. Rust Project Developers – Formalny opis języka, model pamięci, brak klasycznego runtime’u GC.
- Command Line Applications in Rust. PragProg (2022) – Projektowanie i implementacja narzędzi CLI w Ruście.
- clap Documentation. clap-rs Project – Tworzenie interfejsów linii komend, flag, podkomend w Ruście.
- anyhow Documentation. anyhow-rs Project – Praktyczna obsługa błędów w aplikacjach CLI w Ruście.
- indicatif Documentation. indicatif-rs Project – Paski postępu, kolorowy output i metadane w narzędziach terminalowych.
- Rust Analyzer Manual. Rust Analyzer Project – Wsparcie IDE, integracja z toolchainem, diagnostyka błędów kompilacji.






