Files
learn-rust/lessons/0002-write-a-cli-from-blank.html
T

204 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Write a CLI from a blank file</title>
<link rel="stylesheet" href="../assets/style.css" />
<script src="../assets/quiz.js" defer></script>
</head>
<body>
<h1>Write a CLI from a blank file</h1>
<p class="subtitle">Lesson 0002 · production, not recognition · ~25 minutes</p>
<p>Your <a href="0001-diagnostic-ch1-9.html">diagnostic</a> said something more useful than the score: you recognise Rust but can't <em>produce</em> it. That's a different skill, and re-reading the book does not fix it. Only typing does.</p>
<p>So this lesson has no reading section. You will type a working command-line tool from an empty file, running it after every stage. By the end you'll have touched — <em>in your own fingers</em> — types, functions, control flow, <code>match</code>, <code>Result</code>, <code>Vec</code>, and borrowing. Six of your eight weak topics, in one 45-line program.</p>
<div class="callout">
<strong>Keep <a href="../reference/rust-syntax.html">the syntax reference</a> open in another tab.</strong> Looking up syntax is not cheating — a working memory clogged with "how do I write a for loop again" has nothing left for the actual concept. Look it up, type it, move on.
<br /><br />
<strong>Rule for this lesson: type every line by hand.</strong> Do not copy-paste. The muscle memory <em>is</em> the lesson.
</div>
<h2>What you're building</h2>
<p>A grade tool. You pass it scores; it prints a letter for each, skips garbage input, and prints the average:</p>
<pre><code>$ cargo run -- 95 83 71 abc 40
95 -> A
83 -> B
71 -> C
abc -> not a number, skipped
40 -> F
average: 72</code></pre>
<h2>Stage 0 — new project</h2>
<pre><code>cd ~/learn-rust
cargo new grader
cd grader</code></pre>
<p>Open <code>src/main.rs</code>. Cargo wrote a hello-world in it. Delete all of it — blank file.</p>
<h2>Stage 1 — read the arguments</h2>
<p>Type this:</p>
<pre><code>use std::env;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
println!("{args:?}");
}</code></pre>
<p>Run it: <code>cargo run -- 95 83</code></p>
<div class="q" data-type="recall" data-topic="Data Types">
<p class="topic">Predict before you run</p>
<p class="prompt">How many items will be in <code>args</code>, and what is the first one?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Three. <code>args[0]</code> is the path to your own binary — the program name always comes first. Your real input starts at <code>args[1]</code>. Output looks like <code>["target/debug/grader", "95", "83"]</code>.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
</div>
<p>Two things to notice in what you just typed: the type annotation <code>Vec&lt;String&gt;</code> is <em>required</em> here, because <code>.collect()</code> can build many different collections and needs to be told which. And <code>{args:?}</code> uses <code>Debug</code> formatting — <code>{}</code> alone would not compile, because a <code>Vec</code> has no <code>Display</code> impl.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch12-01-accepting-command-line-arguments.html">12.1 Accepting Command Line Arguments</a></p>
<h2>Stage 2 — guard against no input</h2>
<p>Replace the <code>println!</code> with:</p>
<pre><code> if args.len() &lt; 2 {
println!("usage: cargo run -- &lt;score&gt; [more scores...]");
return;
}</code></pre>
<p>Run <code>cargo run</code> with no arguments — you should get the usage line. This is your first <em>trust boundary</em>: never assume input exists. Backend code lives or dies on this habit.</p>
<div class="q" data-type="mcq" data-topic="Data Types">
<p class="topic">Checkpoint</p>
<p class="prompt">What type does <code>args.len()</code> return?</p>
<div class="options">
<button class="opt" data-correct="true">usize</button>
<button class="opt" data-correct="false">u32</button>
<button class="opt" data-correct="false">i32</button>
<button class="opt" data-correct="false">u64</button>
</div>
<div class="explain hidden"><code>usize</code> — the pointer-sized unsigned integer. Every length and index in Rust is <code>usize</code>, which is why mixing it with <code>u32</code> needs an explicit <code>as</code> cast. You'll hit exactly that in Stage 5.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-02-data-types.html">3.2 Data Types</a></p>
</div>
<h2>Stage 3 — parse each argument</h2>
<p>Below the guard, add:</p>
<pre><code> for arg in &amp;args[1..] {
match arg.parse::&lt;u32&gt;() {
Ok(score) =&gt; println!("{score} -&gt; ok"),
Err(_) =&gt; println!("{arg} -&gt; not a number, skipped"),
}
}</code></pre>
<p>Run: <code>cargo run -- 95 abc 40</code>. You should see two <code>ok</code> lines and one skip.</p>
<p>Three weak topics just collided in five lines, so slow down here:</p>
<ul>
<li><code>&amp;args[1..]</code> is a <strong>slice</strong> — a borrowed view of the vector from index 1 onward. You did not copy the arguments and you did not take ownership of them.</li>
<li><code>.parse()</code> returns a <strong><code>Result</code></strong>, because parsing can fail. <code>::&lt;u32&gt;</code> is the turbofish telling it which type to aim for.</li>
<li><code>match</code> forces you to handle <em>both</em> arms. This is the whole point of <code>Result</code>: the compiler will not let you forget the failure case. Compare this to your <code>learn-panic</code> project, where you reached for <code>panic!</code> — here, bad input just gets skipped and the program carries on. That is the difference between a script and a tool.</li>
</ul>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2 Recoverable Errors with Result</a></p>
<h2>Stage 4 — the grading function</h2>
<p>Below <code>main</code>'s closing brace, add a new function:</p>
<pre><code>fn grade(score: u32) -&gt; String {
match score {
90..=100 =&gt; "A".to_string(),
80..=89 =&gt; "B".to_string(),
70..=79 =&gt; "C".to_string(),
_ =&gt; "F".to_string(),
}
}</code></pre>
<p>Now use it — change the <code>Ok</code> arm inside <code>main</code> to:</p>
<pre><code> Ok(score) =&gt; println!("{score} -&gt; {}", grade(score)),</code></pre>
<p>Run: <code>cargo run -- 95 83 71 40</code> → <code>A B C F</code>.</p>
<div class="q" data-type="recall" data-topic="Enums &amp; Pattern Matching">
<p class="topic">Checkpoint</p>
<p class="prompt">Delete the <code>_ =&gt; "F".to_string(),</code> arm and run <code>cargo build</code>. What does the compiler say, and why?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Something like <code>non-exhaustive patterns: `0_u32..=69_u32` and `101_u32..=u32::MAX` not covered</code>. <code>match</code> must handle every possible value of the type — and <code>u32</code> includes 0–69 and everything above 100. The <code>_</code> arm is what makes it exhaustive. <strong>Put the arm back before continuing.</strong></div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 The match Control Flow Construct</a></p>
</div>
<p>Note the return type: <code>String</code>, not <code>&amp;str</code>. The function builds a value and hands ownership to its caller. Returning a borrowed <code>&amp;str</code> here would force you to answer "borrowed from <em>what</em>, and does that thing outlive the caller?" — which is lifetimes, chapter 10, and deliberately not today's problem.</p>
<h2>Stage 5 — collect and average</h2>
<p>Above the <code>for</code> loop, add a vector to accumulate into:</p>
<pre><code> let mut scores: Vec&lt;u32&gt; = Vec::new();</code></pre>
<p>Change the <code>Ok</code> arm to a block, so it can do two things:</p>
<pre><code> Ok(score) =&gt; {
println!("{score} -&gt; {}", grade(score));
scores.push(score);
}</code></pre>
<p>After the loop, add:</p>
<pre><code> if scores.is_empty() {
println!("no valid scores");
} else {
println!("average: {}", average(&amp;scores));
}</code></pre>
<p>And a second function at the bottom of the file:</p>
<pre><code>fn average(scores: &amp;[u32]) -&gt; u32 {
let mut total = 0;
for score in scores {
total += score;
}
total / scores.len() as u32
}</code></pre>
<p>Run: <code>cargo run -- 95 83 71 abc 40</code> → you should get exactly the output from the top of this page, ending in <code>average: 72</code>.</p>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Checkpoint — the important one</p>
<p class="prompt">Change the call to <code>average(scores)</code> and the parameter to <code>scores: Vec&lt;u32&gt;</code>, then try to print <code>scores.len()</code> on the line <em>after</em> that call. What happens, and why does the <code>&amp;</code> version not have this problem?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It fails to compile: <code>borrow of moved value: `scores`</code>. Passing a <code>Vec</code> by value <em>moves</em> ownership into the function, which then drops it at the end — so <code>main</code> has nothing left to read. Passing <code>&amp;scores</code> only <em>borrows</em> it: the function reads it, the borrow ends when the function returns, and <code>main</code> still owns it. This is why function signatures in real Rust take <code>&amp;</code> by default and take ownership only on purpose. <strong>Put the <code>&amp;</code> version back.</strong></div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2 References and Borrowing</a></p>
</div>
<p>Two details worth burning in:</p>
<ul>
<li>The parameter is <code>&amp;[u32]</code>, not <code>&amp;Vec&lt;u32&gt;</code>. A slice accepts a <code>Vec</code>, an array, or part of either — strictly more useful, same speed. Idiomatic Rust prefers <code>&amp;[T]</code> in every read-only signature.</li>
<li><code>scores.len() as u32</code> needs the cast because <code>len()</code> is <code>usize</code> and <code>total</code> is <code>u32</code>. Rust does no implicit numeric conversion, ever.</li>
</ul>
<h2>Stage 6 — a feedback loop that outlives you</h2>
<p>At the very bottom of the file:</p>
<pre><code>#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grades_map_to_letters() {
assert_eq!(grade(95), "A");
assert_eq!(grade(80), "B");
assert_eq!(grade(42), "F");
}
#[test]
fn average_of_three() {
assert_eq!(average(&amp;[90, 80, 70]), 80);
}
}</code></pre>
<p>Run <code>cargo test</code>. Expect <code>2 passed</code>.</p>
<p>Now break something on purpose — change <code>80..=89</code> to <code>81..=89</code> and run <code>cargo test</code> again. One test fails and tells you exactly what it expected. That loop, not the compiler, is what you'll lean on when programs get big enough that you can't hold them in your head. Change it back.</p>
<p>You just met three things at once: <code>#[cfg(test)]</code> (compile this module only during tests), <code>mod tests</code> (a module — the same feature as your <code>restauran</code> project), and <code>use super::*</code> (pull in everything from the parent module, which is how the test sees <code>grade</code>). Testing proper is chapter 11; today it's just the loop.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 How to Write Tests</a></p>
<h2>Your win</h2>
<p>Forty-five lines, typed by hand, that read real input, reject bad input without crashing, and prove themselves with tests. That is a smaller program than your <code>learn-challenges</code> exercise — but you wrote this one from a blank file, which is the thing you said you'd lost.</p>
<div id="summary">
<h2>Checkpoint results</h2>
<div id="summary-body">No checkpoints answered yet.</div>
<p id="summary-total"></p>
<button id="report-btn" disabled>Copy report</button>
<pre id="report-output" class="hidden"></pre>
</div>
<h2>Stretch task (optional, do it before the next lesson)</h2>
<p>Without looking at this page: add a <code>highest</code> function that returns the top score, and print it. You'll need <code>Option</code>, because an empty slice has no maximum. If you get stuck on the signature, that's a real question — ask the agent.</p>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">The Rust Book, ch. 12 — An I/O Project: Building a Command Line Program</a>. It builds a bigger version of exactly what you just wrote, and it's the best next read for a backend/CLI goal.</p>
<p><strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference (ch. 1–9)</a> · <strong>Previous:</strong> <a href="0001-diagnostic-ch1-9.html">0001 Diagnostic</a></p>
<p>Stuck, or got a compiler error this page didn't predict? Paste it to the agent — reading compiler errors fluently is itself a skill worth a lesson, and that's a good excuse to start one.</p>
</footer>
</body>
</html>