CodeBlanks Logo Icon CodeBlanks

CLI

Build list and get subcommands with clap.

CLI

In the last step we connect the queries to the command line. In a real project with clap subcommands:

#[derive(Subcommand)]
enum Komut {
    Liste,
    Etiket { ad: String },
    Yazar { ad: String },
}

In the exercise we build the same structure with slice patterns. Each command turns into a filter, so the listing code stays in one place:

let suzgec: Box<dyn Fn(&Yazi) -> bool> = match args {
    ["liste"] => Box::new(|_| true),
    ["etiket", e] => Box::new(move |y| y.etiketler.iter().any(|x| x == e)),
    ["yazar", a] => Box::new(move |y| y.yazar == *a),
    [k, ..] => return Err(format!("bilinmeyen komut: {k}")),
    [] => return Err(String::from("komut gerekli")),
};

The runner

Once the filter is ready, listing is the same for every command. Command handling returns Result<Vec<String>, String>; main only prints:

fn calistir(v: &[Yazi], args: &[&str]) -> Result<Vec<String>, String> {
    let suzgec = /* the match above */;
    Ok(v.iter()
        .filter(|y| y.yayinda && suzgec(y))
        .map(|y| format!("{}. {}", y.id, y.konu))
        .collect())
}

Practice Blog CLI

With the veri_seti() data, write the calistir function above. Run the commands ["etiket", "web"], ["yazar", "ece"] and ["sil"] in order; print the result lines, or hata: MESSAGE on error.

Starter code

main.rs
#[derive(Debug, Clone)]
struct Yazi {
    id: u32,
    konu: String,
    yazar: String,
    etiketler: Vec<String>,
    yayinda: bool,
}

fn veri_seti() -> Vec<Yazi> {
    let y = |id, konu: &str, yazar: &str, et: &[&str], yayinda| Yazi {
        id,
        konu: konu.to_string(),
        yazar: yazar.to_string(),
        etiketler: et.iter().map(|e| e.to_string()).collect(),
        yayinda,
    };
    vec![
        y(1, "Rust ile başlangıç", "ece", &["rust", "giriş"], true),
        y(2, "Sahiplik", "can", &["rust"], false),
        y(3, "Web sunucusu", "ece", &["rust", "web"], true),
        y(4, "Python notları", "ali", &["python"], true),
    ]
}

fn main() {
    // buraya yaz
}

Sample solution

main.rs
#[derive(Debug, Clone)]
struct Yazi {
    id: u32,
    konu: String,
    yazar: String,
    etiketler: Vec<String>,
    yayinda: bool,
}

fn veri_seti() -> Vec<Yazi> {
    let y = |id, konu: &str, yazar: &str, et: &[&str], yayinda| Yazi {
        id,
        konu: konu.to_string(),
        yazar: yazar.to_string(),
        etiketler: et.iter().map(|e| e.to_string()).collect(),
        yayinda,
    };
    vec![
        y(1, "Rust ile başlangıç", "ece", &["rust", "giriş"], true),
        y(2, "Sahiplik", "can", &["rust"], false),
        y(3, "Web sunucusu", "ece", &["rust", "web"], true),
        y(4, "Python notları", "ali", &["python"], true),
    ]
}

fn calistir(v: &[Yazi], args: &[&str]) -> Result<Vec<String>, String> {
    let suzgec: Box<dyn Fn(&Yazi) -> bool> = match args {
        ["liste"] => Box::new(|_| true),
        ["etiket", e] => {
            let e = e.to_string();
            Box::new(move |y| y.etiketler.iter().any(|x| *x == e))
        }
        ["yazar", a] => {
            let a = a.to_string();
            Box::new(move |y| y.yazar == a)
        }
        [k, ..] => return Err(format!("bilinmeyen komut: {k}")),
        [] => return Err(String::from("komut gerekli")),
    };
    Ok(v.iter()
        .filter(|y| y.yayinda && suzgec(y))
        .map(|y| format!("{}. {}", y.id, y.konu))
        .collect())
}

fn main() {
    let yazilar = veri_seti();
    let komutlar: [&[&str]; 3] = [&["etiket", "web"], &["yazar", "ece"], &["sil"]];
    for k in komutlar {
        match calistir(&yazilar, k) {
            Ok(sonuclar) => {
                for s in sonuclar {
                    println!("{}", s);
                }
            }
            Err(e) => println!("hata: {}", e),
        }
    }
}

Expected output

3. Web sunucusu
1. Rust ile başlangıç
3. Web sunucusu
hata: bilinmeyen komut: sil

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 Why do we keep the filters in a <code>Box&lt;dyn Fn&gt;</code>?

  • Each match arm produces a closure of a different type; a common type is needed Correct answer
  • It's faster
  • Closures can't be called without a Box
  • It isn't needed

Explanation: Every closure is a unique type; all arms of a match expression must return the same type.

2 Common filter type

Question
let suzgec: Box<dyn ____(&Yazi) -> bool> = Box::new(|_| true);
Answer
let suzgec: Box<dyn Fn(&Yazi) -> bool> = Box::new(|_| true);

Explanation: A closure called many times that only reads is Fn.

3 Command and result

Term Meaning
etiket web 3. Web sunucusu
yazar ali 4. Python notları
liste 1, 3, 4
sil error: unknown command

All lessons in Intermediate