Parancssori alkalmazások készítése Rusttal: egy teljes útmutató

Üdvözöllek, fejlesztőtárs! Készen állsz, hogy elmerülj a parancssori alkalmazások (CLI) világában, és mindezt a Rust programozási nyelv erejével tegyed? Akkor jó helyen jársz! Ez a cikk egy átfogó útmutatót kínál a Rust alapú CLI eszközök elkészítéséhez, a kezdeti beállításoktól a disztribúcióig. Megnézzük, miért is a Rust az egyik legjobb választás erre a feladatra, és lépésről lépésre bemutatjuk a legfontosabb eszközöket és technikákat.

Bevezetés: Miért pont a Rust a CLI-hez?

A parancssori eszközök a fejlesztők, rendszeradminisztrátorok és gyakorlatilag mindenki számára elengedhetetlenek, akik automatizálni, kezelni vagy egyszerűen csak interakcióba lépni szeretnének a számítógépes rendszerekkel. Gondoljunk csak a git-re, ls-re, vagy akár a saját fejlesztői eszközeinkre. Ezek az alkalmazások gyakran igénylik a gyors, megbízható és erőforrás-hatékony működést.

Itt jön képbe a Rust! Miért olyan kiváló választás a parancssori alkalmazások fejlesztéséhez?

  • Teljesítmény: A Rust natív kódra fordul, és futásidejű garbage collector nélkül működik, ami rendkívül gyors és hatékony alkalmazásokat eredményez.
  • Memóriabiztonság: A Rust fordítóprogramja garantálja a memóriabiztonságot, elkerülve a gyakori hibákat, mint a null pointer dereferálás vagy adatszinkronizációs problémák, még konkurens környezetben is. Ez kulcsfontosságú a robusztus rendszerek építésénél.
  • Konkurencia: A Rust beépített támogatást nyújt a biztonságos konkurenciához, ami lehetővé teszi, hogy több feladatot is hatékonyan kezeljünk anélkül, hogy aggódnunk kellene a race condition-ök miatt.
  • Platformfüggetlenség: A Rust könnyedén fordítható különböző operációs rendszerekre és architektúrákra, így egyetlen kódbázisból hozhatunk létre futtatható fájlokat Windows, macOS és Linux rendszerekre egyaránt.
  • Fejlesztői élmény: Bár a Rust tanulási görbéje meredek lehet, a beépített csomagkezelő (cargo), a kiváló dokumentáció és a segítőkész közösség hamar felgyorsítja a fejlesztési folyamatot.

Nem véletlen, hogy számos népszerű és nagy teljesítményű CLI eszköz, mint például a ripgrep (gyors fájlkereső), exa (modern ls alternatíva) vagy a bat (szintaktikailag kiemelt cat), Rustban íródott. Csatlakozzunk mi is ehhez a csoporthoz!

Első lépések: A Rust környezet beállítása

Mielőtt belevágnánk a kódolásba, győződjünk meg róla, hogy a Rust megfelelően telepítve van a rendszerünkön. Ha még nem tetted meg, a legegyszerűbb módja a rustup használata:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Ez a parancs telepíti a Rust programozási nyelvet, a cargo csomagkezelőt és számos más hasznos eszközt. A cargo nem csak a fordításért és a függőségek kezeléséért felel, hanem a projektek létrehozásáért és futtatásáért is.

Hozzuk is létre az első Rust CLI projektünket:

cargo new my_cli_app --bin
cd my_cli_app

A --bin flag jelzi, hogy egy futtatható alkalmazást szeretnénk létrehozni. A cargo legenerál egy alapvető projektstruktúrát, benne egy src/main.rs fájllal, ami a „Hello, World!” programot tartalmazza:

// src/main.rs
fn main() {
    println!("Hello, world!");
}

Futtassuk az alkalmazást:

cargo run

Ennek eredményeként a konzolra kiíródik a „Hello, world!”. Gratulálok, az első Rust CLI alkalmazásod már fut!

Parancssori argumentumok kezelése: A CLI lelke

Egy igazi CLI alkalmazásnak képesnek kell lennie a parancssori argumentumok fogadására és feldolgozására. Kezdetben használhatjuk a std::env::args() függvényt, de ez gyorsan bonyolulttá válhat, ahogy nő az argumentumok száma és komplexitása.

Szerencsére a Rust közösség felkínálja a tökéletes megoldást: a `clap` (Command-Line Argument Parser) könyvtárat. A clap egy rendkívül robusztus, hatékony és könnyen használható könyvtár, amely lehetővé teszi a parancssori argumentumok deklaratív módon történő definiálását. A legtöbb komoly Rust CLI alkalmazás a clap-et használja.

A `clap` használata

Először is, add hozzá a clap-et a Cargo.toml fájlodhoz a [dependencies] szekcióba:

[dependencies]
clap = { version = "4.0", features = ["derive"] } # A "derive" feature sokban megkönnyíti a használatot

Most nézzünk egy egyszerű példát, ami egy fájlnevet és egy opcionális flaget fogad:

// src/main.rs
use clap::Parser;

/// Egy egyszerű CLI alkalmazás fájlfeldolgozásra
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// A feldolgozandó fájl neve
    #[arg(short, long)]
    file: String,

    /// Kapcsoló a részletes kimenet engedélyezéséhez
    #[arg(short, long, default_value_t = false)]
    verbose: bool,
}

fn main() {
    let args = Args::parse();

    println!("Fájl neve: {}", args.file);
    if args.verbose {
        println!("Részletes kimenet engedélyezve.");
    } else {
        println!("Részletes kimenet kikapcsolva.");
    }
}

Próbáld ki:

cargo run -- --file adat.txt
cargo run -- -f masik_adat.csv --verbose
cargo run -- --help

Láthatod, hogy a clap automatikusan generálja a súgóüzeneteket, és kezeli a parancssori argumentumok feldolgozását. A #[derive(Parser)] makróval gyakorlatilag deklaráljuk az elvárt argumentumokat egy struktúrában, ami rendkívül olvashatóvá és karbantarthatóvá teszi a kódot.

Alparancsok (Subcommands)

Bonyolultabb alkalmazások esetén, ahol több különböző műveletet is végrehajthatunk (pl. git add, git commit), érdemes alparancsokat használni. A clap ezt is elegánsan kezeli:

// src/main.rs
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(author, version, about = "Egy képkezelő alkalmazás", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Kép konvertálása másik formátumba
    Convert {
        /// A bemeneti fájl elérési útja
        #[arg(short, long)]
        input: String,

        /// A kimeneti fájl elérési útja
        #[arg(short, long)]
        output: String,

        /// Az új formátum (pl. png, jpg)
        #[arg(short, long)]
        format: String,
    },
    /// Kép átméretezése
    Resize {
        /// A bemeneti fájl elérési útja
        #[arg(short, long)]
        input: String,

        /// Az új szélesség pixelekben
        #[arg(short, long)]
        width: u32,

        /// Az új magasság pixelekben
        #[arg(short, long)]
        height: u32,
    },
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Convert { input, output, format } => {
            println!("Kép konvertálása: {} -> {} (Formátum: {})", input, output, format);
            // Itt jönne a tényleges konvertálási logika
        },
        Commands::Resize { input, width, height } => {
            println!("Kép átméretezése: {} (Új méret: {}x{})", input, width, height);
            // Itt jönne a tényleges átméretezési logika
        },
    }
}

Példák a futtatásra:

cargo run -- convert -i kep.jpg -o kep.png -f png
cargo run -- resize -i nagy_kep.png --width 800 --height 600
cargo run -- help convert

Ez a struktúra sokkal tisztábbá teszi az alkalmazás logikáját és a felhasználó számára is könnyebben értelmezhetővé válik.

Bevitel és kivitel (I/O): Kommunikáció a külvilággal

A CLI alkalmazások gyakran kommunikálnak a felhasználóval vagy más programokkal az input/output (I/O) csatornákon keresztül. A Rust standard könyvtára (std::io) bőségesen elegendő ehhez.

Standard I/O

  • stdout (standard output): Ide írjuk ki az alkalmazás normál kimenetét (pl. println!).
  • stderr (standard error): Ide írjuk ki a hibaüzeneteket és figyelmeztetéseket (pl. eprintln!). Ez fontos, mert lehetővé teszi, hogy a felhasználó külön kezelje a normál kimenetet és a hibaüzeneteket (pl. átirányítás).
  • stdin (standard input): Innen olvassuk be a felhasználó által begépelt adatokat vagy egy másik program kimenetét.
use std::io::{self, Write};

fn main() {
    // Kiírás standard outputra
    println!("Ez egy normál üzenet.");

    // Kiírás standard errorra
    eprintln!("Ez egy hibaüzenet!");

    // Felhasználói bevitel olvasása
    print!("Kérlek, add meg a neved: ");
    io::stdout().flush().unwrap(); // Fontos, hogy kiürítsük a puffert, mielőtt beolvasunk

    let mut nev = String::new();
    io::stdin().read_line(&mut nev).expect("Nem sikerült beolvasni a sort.");

    println!("Szia, {}!", nev.trim()); // trim() eltávolítja a sortörést
}

Fájlkezelés

A CLI alkalmazások gyakran dolgoznak fájlokkal. A Rust std::fs modulja egyszerű és hatékony fájlkezelési funkciókat biztosít.

use std::fs;
use std::io::{self, Read, Write};

fn main() -> io::Result { // A main függvény Result-ot ad vissza a hibakezeléshez
    let filename = "pelda.txt";
    let content = "Ez egy példa fájl tartalma.nRusttal íródott.";

    // Fájl írása
    fs::write(filename, content)?; // A ? operátor propagálja a hibát

    println!("'{}' sikeresen létrejött és feltöltődött.", filename);

    // Fájl olvasása
    let read_content = fs::read_to_string(filename)?;
    println!("'{}' tartalma:n{}", filename, read_content);

    // Fájl törlése
    fs::remove_file(filename)?;
    println!("'{}' sikeresen törölve.", filename);

    Ok(())
}

Fontos, hogy a fájlműveletek hibát dobhatnak (pl. ha a fájl nem létezik, vagy nincs írási jogunk), ezért a hibakezelés itt különösen fontos. Erre hamarosan visszatérünk.

Hibakezelés: Robusztus alkalmazások építése

A Rust egyik kiemelkedő tulajdonsága a kifinomult hibakezelés, amely a Result enumra épül. Ez biztosítja, hogy a hibákat explicit módon kezeljük, elkerülve a váratlan összeomlásokat.

A Result két variánssal rendelkezik:

  • Ok(T): a művelet sikeres volt, és visszaadja a T típusú értéket.
  • Err(E): a művelet hibával zárult, és visszaadja az E típusú hibaobjektumot.

A ? operátor rendkívül hasznos a hibák propagálásában. Ha egy Result típusú érték Err variánsa, a függvény azonnal visszatér az adott hibával. Ha Ok, akkor kicsomagolja az értéket és folytatódik a végrehajtás.

use std::fs;
use std::io;

fn read_file_content(path: &str) -> Result {
    let content = fs::read_to_string(path)?; // A ? operátor itt propagálja az io::Error-t
    Ok(content)
}

fn main() {
    let filename = "nem_letezo_fajl.txt";
    match read_file_content(filename) {
        Ok(content) => println!("Fájl tartalma:n{}", content),
        Err(e) => eprintln!("Hiba a fájl olvasásakor '{}': {}", filename, e),
    }

    let existing_file = "pelda.txt";
    // Hozzunk létre egy fájlt a példa kedvéért
    fs::write(existing_file, "Ez egy létező fájl.").unwrap();

    match read_file_content(existing_file) {
        Ok(content) => println!("Fájl tartalma:n{}", content),
        Err(e) => eprintln!("Hiba a fájl olvasásakor '{}': {}", existing_file, e),
    }
    fs::remove_file(existing_file).unwrap(); // Töröljük a fájlt
}

Külső hibakezelő könyvtárak: `anyhow` és `thiserror`

Komplexebb alkalmazásokban sokféle hiba fordulhat elő. A anyhow és thiserror könyvtárak egyszerűsítik a hibakezelést:

  • `anyhow`: Gyors és egyszerű hiba típusok létrehozására és kezelésére. Akkor ideális, ha a hiba pontos típusa nem lényeges, csak az, hogy valami rosszul sült el.
  • `thiserror`: Strukturált, egyedi hiba típusok definiálására. Akkor hasznos, ha pontosan meg akarjuk adni a hiba okát, és programozottan akarunk reagálni rájuk.

Példa anyhow használatára:

[dependencies]
anyhow = "1.0"
// src/main.rs
use anyhow::{Context, Result};
use std::fs;

fn process_file(path: &str) -> Result {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Nem sikerült beolvasni a fájlt: {}", path))?;
    println!("Feldolgozott tartalom: {}", content);
    // Itt történhetne valami további feldolgozás
    Ok(())
}

fn main() {
    if let Err(e) = process_file("nem_letezo_fajl.txt") {
        eprintln!("Alkalmazás hiba: {:?}", e);
    }
    // Hozzunk létre egy fájlt a példa kedvéért
    fs::write("valami.txt", "Ez egy teszt.").unwrap();
    if let Err(e) = process_file("valami.txt") {
        eprintln!("Alkalmazás hiba: {:?}", e);
    }
    fs::remove_file("valami.txt").unwrap();
}

A .with_context() metódussal további kontextust adhatunk a hibákhoz, ami jelentősen megkönnyíti a hibakeresést.

Naplózás: Amikor a dolgok rosszra fordulnak (vagy csak megfigyeljük őket)

A CLI alkalmazásokban is fontos lehet a naplózás, különösen hosszabb futású vagy háttérben futó eszközök esetén. A Rust ökoszisztémában a `log` crate egy facádként (interface-ként) szolgál a naplózáshoz, míg az `env_logger` egy népszerű implementációja ennek.

Adjuk hozzá a Cargo.toml fájlhoz:

[dependencies]
log = "0.4"
env_logger = "0.10"

Példa használatra:

use log::{info, warn, error, debug};
use env_logger::Env;

fn main() {
    // Inicializáljuk az env_logger-t.
    // Az `Env::default()` a RUST_LOG környezeti változót figyeli.
    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

    info!("Az alkalmazás elindult.");
    debug!("Ez egy hibakeresési üzenet. Csak debug módban látható.");

    let user_input = "teszt"; // Tegyük fel, hogy ezt kaptuk bemenetként
    if user_input.is_empty() {
        warn!("Üres bemenetet kapott.");
    } else {
        info!("Bemenet feldolgozása: {}", user_input);
        // ... további logika ...
    }

    if user_input == "hiba" {
        error!("Kritikus hiba történt a 'hiba' bemenettel.");
    }

    info!("Az alkalmazás befejeződött.");
}

Futtatáskor a RUST_LOG környezeti változóval szabályozhatjuk a naplózás részletességét:

cargo run # Csak info, warn, error üzenetek
RUST_LOG=debug cargo run # info, warn, error, debug üzenetek
RUST_LOG=trace cargo run # Még részletesebb (trace) üzenetek is

Haladó funkciók: Felhasználóbarát CLI-k

A felhasználóbarát CLI eszközök nem csak funkcionálisak, hanem esztétikusak és interaktívak is lehetnek. Íme néhány népszerű crate, amelyek segítenek ebben:

Színes kimenet

A `colored` vagy `owo-colors` crate segítségével könnyedén színesíthetjük a konzol kimenetet, ami javítja az olvashatóságot és vizuálisan kiemeli a fontos információkat.

[dependencies]
colored = "2.0"
use colored::*;

fn main() {
    println!("{}", "Ez egy piros hibaüzenet.".red());
    println!("{}", "Ez egy zöld sikeres üzenet.".green().bold());
    println!("Ez a szöveg {} és {}", "kék".blue(), "aláhúzott".underline());
}

Folyamatjelző sávok

Hosszabb ideig tartó műveletek esetén a `indicatif` crate segítségével elegáns folyamatjelző sávokat (progress bars) jeleníthetünk meg, ami visszajelzést ad a felhasználónak a folyamatról.

[dependencies]
indicatif = "0.17"
use indicatif::{ProgressBar, ProgressStyle};
use std::thread;
use std::time::Duration;

fn main() {
    let pb = ProgressBar::new(100);
    pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})")
        .unwrap()
        .progress_chars("#>-"));

    for i in 0..100 {
        pb.inc(1);
        thread::sleep(Duration::from_millis(50));
    }
    pb.finish_with_message("Feldolgozás kész!");
}

Interaktív promptok

A `inquire` vagy `dialoguer` crate lehetővé teszi interaktív kérdések (szöveg bevitele, listából választás, igen/nem kérdések) feltevését a felhasználónak.

[dependencies]
inquire = "0.6"
use inquire::{Text, Confirm, Select};

fn main() {
    let name = Text::new("Mi a neved?").prompt().unwrap();
    println!("Szia, {}!", name);

    let confirm = Confirm::new("Folytatni szeretnéd?").with_default(true).prompt().unwrap();
    if !confirm {
        println!("Oké, viszlát!");
        return;
    }

    let options = vec!["Alma", "Körte", "Szilva"];
    let fruit = Select::new("Válassz egy gyümölcsöt:", options).prompt().unwrap();
    println!("A választott gyümölcs: {}", fruit);
}

Konfigurációs fájlok

Sok CLI alkalmazás igényel konfigurációs beállításokat, amiket fájlból (pl. YAML, JSON, TOML) olvas be. A `serde` (szerializálás/deszerializálás keretrendszer) és a `serde_yaml` (YAML formátumhoz) vagy `confy` (egyszerű konfiguráció kezelő) ideálisak erre a célra.

Tesztelés: Hogy a kódod működjön, ahogy elvárjuk

A tesztelés elengedhetetlen a megbízható szoftverek építéséhez. A Rust beépített tesztelési keretrendszerrel rendelkezik, ami támogatja az egységteszteket és az integrációs teszteket is.

Egységtesztek

Az egységtesztek általában ugyanabban a fájlban vannak, mint a tesztelt kód, egy #[cfg(test)] attribútummal ellátott modulban.

// src/main.rs (vagy src/lib.rs)
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*; // Hogy hozzáférjünk a szülő modul elemeihez

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
        assert_eq!(add(0, 0), 0);
        assert_eq!(add(-1, 1), 0);
    }
}

Futtatás: cargo test

Integrációs tesztek

Az integrációs tesztek a teljes alkalmazást tesztelik, a külső interfészeken keresztül. Ezeket külön, a projekt gyökérkönyvtárában lévő tests/ mappába helyezzük.

Hozd létre a tests/cli.rs fájlt:

// tests/cli.rs
use assert_cmd::prelude::*; // Szükséges a .unwrap() és .assert() metódusokhoz
use predicates::prelude::*; // Szükséges a predikátumokhoz (pl. .stdout())
use std::process::Command; // Szükséges a Command-hoz

#[test]
fn runs_with_no_arguments() -> Result<(), Box> {
    let mut cmd = Command::cargo_bin("my_cli_app")?; // "my_cli_app" a Cargo.toml-ban definiált bináris neve
    cmd.assert()
        .failure() // Elvárjuk, hogy hibával térjen vissza, ha nincs argumentum
        .stderr(predicate::str::contains("The following required arguments were not provided")); // Ellenőrizzük a hibaüzenetet
    Ok(())
}

#[test]
fn runs_with_file_argument() -> Result<(), Box> {
    let mut cmd = Command::cargo_bin("my_cli_app")?;
    cmd.arg("--file").arg("test.txt");
    cmd.assert()
        .success() // Elvárjuk, hogy sikeresen fusson
        .stdout(predicate::str::contains("Fájl neve: test.txt")); // Ellenőrizzük a kimenetet
    Ok(())
}

Ehhez a Cargo.toml fájlba a [dev-dependencies] szekcióba fel kell venned az assert_cmd és predicates csomagokat:

[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"

Az integrációs tesztek futtatása szintén cargo test paranccsal történik.

Disztribúció: Eljuttatni a felhasználókhoz

Miután elkészült és tesztelted az alkalmazásodat, eljött az ideje, hogy mások is használhassák.

Fordítás kiadásra

A cargo build --release parancs optimalizált bináris fájlt hoz létre a target/release/ mappába. Ez a bináris sokkal gyorsabb és kisebb lesz, mint a debug verzió.

Keresztfordítás (Cross-compilation)

Ha az alkalmazásodat más operációs rendszerekre vagy architektúrákra is szánod, a Rust támogatja a keresztfordítást. Először is add hozzá a kívánt célplatformot:

rustup target add x86_64-pc-windows-gnu # Windows 64-bit
rustup target add aarch64-apple-darwin # macOS ARM (M1/M2)
rustup target add x86_64-unknown-linux-musl # Linux static binary

Majd fordítsd le a célplatformra:

cargo build --release --target x86_64-pc-windows-gnu

Ez létrehozza a Windows-ra szánt .exe fájlt.

Terjesztési módszerek

  • Bináris fájlként: A legegyszerűbb, ha a lefordított bináris fájlt (esetleg egy zip archívumban) megosztod.
  • `crates.io`-n keresztül: Ha az alkalmazásod egy könyvtár, vagy ha a felhasználók rendelkeznek Rust környezettel, feltöltheted a crates.io-ra, ahonnan cargo install my_cli_app paranccsal telepíthető.
  • Csomagkezelők (Homebrew, APT, stb.): A fejlettebb disztribúcióhoz érdemes lehet csomagkezelőkhöz (pl. Homebrew macOS-en, APT/DNF Linuxon) csomagokat készíteni. Ehhez általában további eszközökre (pl. cargo-deb, cargo-rpm) vagy manuális konfigurációra van szükség.
  • Docker konténerként: Ha az alkalmazásodnak specifikus környezeti függőségei vannak, vagy egyszerűen csak garantálni szeretnéd az azonos futási környezetet, egy Docker image létrehozása kiváló megoldás lehet.

Gyakorlati tippek és legjobb gyakorlatok

  • Közérthető súgó: Mindig biztosíts részletes és érthető súgóüzeneteket a --help és alparancsokhoz is. A clap ebben sokat segít.
  • Verziószám: A -V vagy --version kapcsolóval jelenítsd meg az alkalmazás verzióját. Ezt szintén a clap automatikusan kezeli, ha beállítod a Cargo.toml-ban.
  • Idempotencia: Ha egy parancsot többször is lefuttatunk ugyanazokkal a paraméterekkel, az eredménynek ugyanannak kell lennie, és nem szabad nem kívánt mellékhatásokat okoznia (amennyiben lehetséges).
  • Kimenet formázása: Gondolj arra, hogy a kimenetedet ember vagy gép fogja-e olvasni. Különböző formátumokat (pl. JSON, CSV) is támogathatsz a gépi feldolgozáshoz.
  • Szenzitív adatok kezelése: Soha ne írj ki érzékeny adatokat a konzolra vagy a naplófájlokba. Használj környezeti változókat vagy biztonságos beviteli módszereket jelszavakhoz.
  • Folyamatos integráció/Folyamatos szállítás (CI/CD): Automatizáld a tesztelést, fordítást és disztribúciót CI/CD pipeline-ok (pl. GitHub Actions, GitLab CI) segítségével.

Összefoglalás és további lépések

Gratulálok! Most már rendelkezel azokkal az alapvető ismeretekkel és eszközökkel, amelyekre szükséged van a Rust alapú parancssori alkalmazások építéséhez. Láthattuk, hogy a Rust a teljesítmény, biztonság és a fejlesztői élmény egyedülálló kombinációját kínálja, ami ideálissá teszi CLI eszközök fejlesztéséhez.

Ne habozz kísérletezni! Kezdj egy kis projekttel, próbáld ki a különböző crate-eket, és építsd fel a saját hasznos eszközeidet. A Rust közösség rendkívül aktív és segítőkész, számos kiváló példával és dokumentációval.

A következő lépésekben érdemes lehet tovább mélyedni a tokio aszinkron futtatókörnyezetbe, ha hálózati vagy I/O-intenzív alkalmazásokat szeretnél építeni, vagy felfedezni további speciális crate-eket, amelyek egyedi igényeidet szolgálhatják. Jó kódolást!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük