The fastest way I know to learn a language is to rebuild something you already understand. You stop fighting the problem and spend your attention on the syntax and the idioms. That is the whole idea behind the new Unix tools track I just released on the Rust platform: ten small command-line classics, each one a pure function you implement and cargo test to validate you got it right.

Why Unix tools, and why from Python

Every exercise opens with the Python you would write, then teaches the Rust idiom that replaces it. People who love the platform keep pointing at the same thing: the Python-to-Rust bridge is what makes the concepts stick.

You are not memorizing Iterator methods in the abstract, you are watching len(text.split()) turn into .split_whitespace().count(), and reaching for Option and Result instead of Python's exceptions.

Take cut. In Python an invalid field raises an exception at runtime:

def cut(text, delim, field):  # field is 1-based
    if field == 0:
        raise ValueError("field values may not include zero")
    ...

In Rust both outcomes live in the return type:

#[derive(Debug, PartialEq)]
enum CutError {
    ZeroField,
}

fn cut(text: &str, delim: char, field: usize) -> Result<Vec<&str>, CutError> {
    // ...
}

#[test]
fn field_zero_is_an_error() {
    assert_eq!(cut("a:b", ':', 0), Err(CutError::ZeroField));
}

Encoding the failure in the type, not just the docs, is a core Rust idea: the compiler makes every caller account for it, and that is a big part of what makes Rust code safer. The failing case becomes a test you code towards.

Iterators are the spine of the track, seven of the ten exercises turn for loops and comprehensions into iterator chains. Every exercise relates back to one or more Python idioms you already know.

For me Rust is hard but thanks to comparison with Python I feel that I understand more. - Piotr R

Having a direct comparison with Python snippets keeps me more in the context of what's going on. - Michal S

The track also follows one rule that matters for real CLIs: the logic lives in a pure, testable function, while the I/O (reading a file or stdin) stays in a thin wrapper around it.

That split is exactly how you would structure a professional Rust tool, and it is why every exercise can be validated by a test instead of by running a binary. I wrote more about how this rewiring changes your Python instincts in Rust made me a better Python developer.

The 10 exercises and what each one teaches

  1. wc: count lines, words, characters. Iterators plus .count() replace len(...), and you return a (usize, usize, usize) tuple. The character count is also a sneak intro to why chars().count() is not len() in a Unicode world.

  2. head & tail: first and last N lines. head uses lazy .take(n) and stops early; tail forces you to collect first and slice from the end, because you cannot run an iterator backwards.

  3. cat -n: number the lines. Rust's .enumerate() starts at 0, not 1 (there is no start= argument), and you rebuild the numbered text from there.

  4. tr: translate and delete characters. The exercise that makes the char vs &str distinction click: 'l' is a char, "l" is a &str, and you .map() over .chars() to rewrite each one.

  5. grep: filter matching lines, with -i and -v. Substring tests with .contains(), case folding, and a single boolean condition that handles both -i and -v without branching. First taste of borrowing and lifetimes in the signature.

The Unix tools track on the Rust platform: all 10 exercises from wc to the top_words capstone, with difficulty levels and completion status

  1. cut: extract a field. Two outcomes that pull in different directions: a missing field is normal (skip the line), but field == 0 is a bad request. You model that with Option and Result instead of Python's exceptions.

  2. uniq -c: count adjacent duplicates. Rust has no itertools.groupby, so you walk runs by hand with pattern matching and a Vec, keeping the consecutive-only behavior honest.

  3. sort: sort lines, with -n. Iterators do not sort, so you collect into a mut Vec and call .sort(); numeric order means sort_by_key with a closure, Rust's answer to Python's key=.

  4. sed s///: find and replace per line. str::replace swaps every match, replacen stops after a count, so the g flag is just a choice between two methods, no manual counting.

  5. top_words: the capstone. Compose everything: count word frequencies with the HashMap entry API (entry().or_insert(), since there is no Counter), then sort and take the top n. This is the tr | sort | uniq -c | sort -rn | head pipeline rebuilt as one Rust function.


If you have been meaning to get past reading about Rust to actually writing it and making the concepts stick, begin with the free wc and head / tail exercises. Each one is short, has tests to code towards, and there is no AI on the platform; you have to do the work. I hope you learn a lot: start the Unix tools track.

Next up on the platform: a track on Rust lifetimes.