Skip to content
Learn Rust
Theme

Hello World

Basics · Lesson 7

Our first program prints a line. Put this in src/main.rs and run cargo run.

Coming from Gofn main in src/main.rs is your func main in package main, and println! is closest to fmt.Printf with the \n added: the first argument is a format string, so fmt.Println("x =", x) becomes println!("x = {x}"). cargo run is go run, and Cargo.toml is where go.mod's name and requirements go. The ! means a macro, which lets the compiler check the format string while it builds.

Coming from Kotlinfn main and println! read just like Kotlin's fun main and println. The ! marks a macro, and cargo run builds a native executable: there is no JVM and no jar to run it.

Coming from Pythonprintln! is your print, but its first argument is a format string, not a list of values: print("x =", x) becomes println!("x = {x}"). Code can't sit at the top level of the file: everything starts from fn main, with no if __name__ == "__main__". cargo run compiles the program first, then runs it.

Coming from TypeScriptprintln! is your console.log, but its first argument is a format string, not a list of values: console.log("x =", x) becomes println!("x = {x}"). The ! marks a macro. There is no node or tsc step: cargo run compiles a native executable and runs that, and Cargo.toml plays the part of package.json.

Coming from Javafn main() is your public static void main(String[] args). Java 25 lets a single-file program drop the class and static, and Rust never had either: main is a free function and Rust has no classes. println! is a macro, not a method, so the compiler checks the format string and its arguments before the program runs.

Coming from C#fn main() is your static void Main, written out: Rust has no top-level statements, so every statement must be inside a function. println! is Console.WriteLine, and the ! marks a macro that checks the {} placeholders against the arguments at build time.

Coming from Cfn main() is int main(void) and println! is printf with the \n added for you. A wrong or missing argument for a {} is a compile error, where a wrong printf argument is undefined behaviour, and there is no return 0 to write.

Coming from C++println! plays the role of std::cout << ... << "\n", with {} placeholders like C++20 std::format. Like std::format, a bad format string or a missing argument fails to compile. main has no return 0 and no int.

The quick comparison

Rustsrc/main.rsRun ↗
fn main() {
println!("hello world");
}
$ cargo run
hello world
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
$ go run main.go
hello world
fun main() {
println("hello world")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
hello world
print("hello world")
$ python3 main.py
hello world
console.log("hello world");
$ tsc --strict --module nodenext --types node main.ts && node main.js
hello world
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
}
}
$ javac Main.java && java Main
hello world
Console.WriteLine("hello world");
$ dotnet run
hello world
#include <stdio.h>
int main(void) {
printf("hello world\n");
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
hello world
#include <iostream>
int main() {
std::cout << "hello world\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
hello world

What's different

  • fn main() is where the program starts. It takes no parameters and, by default, returns nothing. Nothing outside a function runs: a println! at the top of the file is an error.
  • println! writes a line to standard output; eprintln! writes to standard error. The ! marks a macro: Macros, attributes, naming has the format-string syntax.
  • cargo new hello makes a project, cargo run builds and runs it, and cargo check only looks for errors, which is faster. The executable lands in target/debug/.
Gotcha

Giving main a return value

Looks fine: in C#, C and C++ it runs fine; in Kotlin and Java it crashes at runtime. Rust won't compile it.

Go refuses this too. Same idea, different words.Broken in Kotlin. Rust catches it.No direct equivalent in Python. Just a Rust rule.No direct equivalent in TypeScript. Just a Rust rule.Broken in Java. Rust catches it.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
fn main() -> i32 {
println!("hello world");
0
}
Go✗ refuses too

Go refuses: "func main must have no arguments and no return values". To set an exit code, call os.Exit(n).

package main
import "fmt"
func main() int {
fmt.Println("hello world")
return 0
}
# command-line-arguments
./main.go:5:6: func main must have no arguments and no return values
fun main(): Int {
println("hello world")
return 0
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar! crashes at runtime
no main manifest attribute, in main.jar
Pythonno equivalent

Python has no main to return from: the file runs from the top. The exit code comes from sys.exit(n).

TypeScriptno equivalent

TypeScript has no main either. Set process.exitCode = n, or call process.exit(n).

public class Main {
public static int main(String[] args) {
System.out.println("hello world");
return 0;
}
}
$ javac Main.java && java Main! crashes at runtime
Error: Main method not found in class Main, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
Console.WriteLine("hello world");
return 0;
$ dotnet run✓ runs
hello world
#include <stdio.h>
int main(void) {
printf("hello world\n");
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
hello world
#include <iostream>
int main() {
std::cout << "hello world\n";
return 0;
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
hello world
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0277?
error[E0277]: `main` has invalid return type `i32`
 --> src/main.rs:1:14
  |
1 | fn main() -> i32 {
  |              ^^^ `main` can only return types that implement `Termination`
  |
  = help: consider using `()`, or a `Result`
What happens in Go

Go refuses: "func main must have no arguments and no return values". To set an exit code, call os.Exit(n).

What happens in Kotlin

Kotlin compiles fun main(): Int, but built with kotlinc -include-runtime the jar has no entry point and java -jar fails with "no main manifest attribute". Kotlin's main returns nothing; call exitProcess(n) to choose a code.

What happens in Python

Python has no main to return from: the file runs from the top. The exit code comes from sys.exit(n).

What happens in TypeScript

TypeScript has no main either. Set process.exitCode = n, or call process.exit(n).

What happens in Java

public static int main compiles, then the JVM reports "Main method not found in class Main" and exits with 1. Java's main must return void; call System.exit(n).

What happens in C#

C# accepts this: a top-level return 0; (or an int return from Main) sets the exit code. Rust gets the same result with ExitCode.

What happens in C

This is plain C: main returns an int, and 0 means success. In Rust the equivalent is ExitCode, and returning () already means 0.

What happens in C++

This is plain C++: main returns an int, and return 0 may be left out because the compiler adds it. Rust's equivalent is ExitCode.

What Rust sees

main may return (), a Result, or an ExitCode. An i32 is none of those, so the compiler says main has an invalid return type and suggests () or a Result.

Why Rust says no

Rust's main can return anything that implements the Termination trait, and that trait decides the exit code. () means success, so most programs return nothing. To pick a code, return std::process::ExitCode; to fail with a message, return a Result (the lesson shows both). A bare number is not accepted, so the code cannot come from a stray value.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
println!("hello world");
}
$ cargo run
hello world

The toolkit

This page covers what sits between typing cargo run and seeing a line: the commands, the project folder, and main. Format strings are in Macros, attributes, naming; arguments and environment variables are in Arguments, Environment, Time and Processes. Outputs come from Linux on 64-bit ARM with Rust and Cargo 1.99.1. Phrasebooks: C and C++, C#, Go, Java, Kotlin, Python, TypeScript. The programs that run cargo build a throwaway project in a temporary folder and trim the build times and that folder’s name; read their output, not their code.

The commands

Command What it does
cargo new hello Makes a folder hello/ with a project in it.
cargo run Builds if anything changed, then runs the executable.
cargo build Builds only. The executable goes in target/debug/.
cargo check Looks for compile errors and makes no executable.
cargo build --release, cargo run --release The optimised build, in target/release/.
rustc main.rs The compiler by itself, for one file with no project.
cargo add, cargo test, cargo fmt, cargo clippy Add a dependency; run tests; format; lint.
Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
// Runs `cargo <args>` in `dir` and prints what a terminal would show. Two things are
// trimmed: the build time ("in 0.06s") and the folder the project lives in.
fn cargo(dir: &Path, args: &[&str]) {
println!("$ cargo {}", args.join(" "));
let out = Command::new("cargo")
.args(args)
.current_dir(dir)
.output()
.unwrap();
let text =
String::from_utf8_lossy(&out.stderr).into_owned() + &String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
let line = line.replace(dir.parent().unwrap().to_str().unwrap(), "~/projects");
match line.find(" target(s) in ") {
Some(i) => println!("{} target(s)", &line[..i]),
None => println!("{line}"),
}
}
}
// The visible entries of a folder, folders marked with a slash.
fn ls(dir: &Path) -> String {
let mut names: Vec<String> = fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap())
.filter(|e| !e.file_name().to_string_lossy().starts_with('.'))
.map(|e| {
let slash = if e.path().is_dir() { "/" } else { "" };
format!("{}{slash}", e.file_name().to_string_lossy())
})
.collect();
names.sort();
names.join(" ")
}
fn main() {
let root = std::env::temp_dir().join("hw-new-run");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
cargo(&root, &["new", "hello"]);
let dir = root.join("hello");
println!("ls hello: {}", ls(&dir));
println!(
"--- Cargo.toml ---\n{}",
fs::read_to_string(dir.join("Cargo.toml")).unwrap()
);
println!(
"--- .gitignore ---\n{}",
fs::read_to_string(dir.join(".gitignore")).unwrap()
);
println!(
"--- src/main.rs ---\n{}",
fs::read_to_string(dir.join("src/main.rs")).unwrap()
);
cargo(&dir, &["run"]);
println!("ls hello: {}", ls(&dir));
println!("ls hello/target: {}", ls(&dir.join("target")));
println!("ls hello/target/debug: {}", ls(&dir.join("target/debug")));
}
$ cargo run
$ cargo new hello
    Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
ls hello: Cargo.toml  src/
--- Cargo.toml ---
[package]
name = "hello"
version = "0.1.0"
edition = "2024"

[dependencies]

--- .gitignore ---
/target

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

$ cargo run
   Compiling hello v0.1.0 (~/projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s)
     Running `target/debug/hello`
Hello, world!
ls hello: Cargo.lock  Cargo.toml  src/  target/
ls hello/target: CACHEDIR.TAG  debug/
ls hello/target/debug: build/  deps/  examples/  hello  hello.d  incremental/

Cargo.toml is the manifest: the package’s name, version, edition (see Editions) and dependencies, none yet. src/main.rs is the root of the program and holds fn main. A crate is one unit of compilation. Two more paths matter:

Path What it is
Cargo.lock Appears after the first build or check: the exact version of every dependency used (the Book).
target/ Everything the build makes. The .gitignore that cargo new wrote lists it.

Your first project walks through the same commands with Cargo’s progress lines.

fn main

Execution starts at fn main, which has no parameters and by default returns (), unit, the value for “nothing”. Command-line arguments come from std::env::args(), whose first item is the program itself, as in C, so .nth(1) (used by the programs below) is the first real argument (Arguments, Environment, Time and Processes runs it). With Cargo, give the program its arguments after --, as in cargo run -- a b (cargo run).

Standard output and standard error

A program has two output streams; a terminal shows both, and cmd > out.txt keeps only the first.

Macro Stream Newline Use it for
print!, println! standard output no, yes The program’s result.
eprint!, eprintln! standard error no, yes Errors, warnings, progress.
dbg! standard error yes Debugging. Prints [file:line:column] expression = value and returns the value.

The program below prints on both streams, then runs itself again to catch each separately; a terminal would show them mixed.

Rustsrc/main.rsRun ↗
use std::process::Command;
// The program under test: it prints on both streams.
fn child() {
print!("loading... ");
println!("done");
eprintln!("warning: cache is cold");
let doubled = dbg!(21 * 2);
println!("doubled: {doubled}");
}
// This same program runs itself with the argument `child` and keeps the two streams
// apart, which a terminal does not: it shows them mixed.
fn main() {
if std::env::args().nth(1).as_deref() == Some("child") {
return child();
}
let out = Command::new(std::env::current_exe().unwrap())
.arg("child")
.output()
.unwrap();
println!("exit code: {:?}", out.status.code());
println!("--- stdout ---\n{}", String::from_utf8_lossy(&out.stdout));
println!("--- stderr ---\n{}", String::from_utf8_lossy(&out.stderr));
}
$ cargo run
exit code: Some(0)
--- stdout ---
loading... done
doubled: 42

--- stderr ---
warning: cache is cold
[src/main.rs:8:19] 21 * 2 = 42

dbg!(21 * 2) printed its source location and expression, and doubled still got 42. It uses the Debug form, the programmer-facing way to print a value ({:?}, in Macros).

main can return a result

main may also return a Result (usually Result<(), E>), whose failure case ? can return early (Errors covers ?). The error is printed on standard error and the exit code is 1:

Rustsrc/main.rs! exits with code 1Run ↗
use std::fs;
fn main() -> Result<(), std::io::Error> {
println!("reading config");
let text = fs::read_to_string("settings.toml")?;
println!("{} bytes", text.len());
Ok(())
}
$ cargo run
reading config
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

To pick the exit code yourself, return a std::process::ExitCode:

Rustsrc/main.rs! exits with code 3Run ↗
use std::process::ExitCode;
fn main() -> ExitCode {
println!("checking...");
ExitCode::from(3)
}
$ cargo run
checking...

A panic (a crash with a message) is a third way to end:

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let items: Vec<i32> = Vec::new();
println!("first: {}", items[0]);
}
$ cargo run
thread 'main' (1) panicked at src/main.rs:3:32:
index out of bounds: the len is 0 but the index is 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
main ends with Exit code Printed on standard error
() 0
Ok(()) 0
Err(e) 1 Error: and the Debug form of e
ExitCode::SUCCESS 0
ExitCode::from(n), with n a u8 (0 to 255) n
a panic 101 the message

The rule is the Termination trait (a named ability a type has: Traits, later), which (), ExitCode and Result<T, E> (when T implements it too and E has a Debug form) implement. Anything else, such as an i32, is the compile error in the gotcha at the top of this page.

To end the program from deep inside, call std::process::exit(n):

Rustsrc/main.rs! exits with code 4Run ↗
fn leave() {
println!("leaving from a nested call");
std::process::exit(4);
}
fn main() {
leave();
println!("not printed");
}
$ cargo run
leaving from a nested call

Its docs say no destructors run (no cleanup code for values still alive) and prefer returning from main.

Debug and release builds

Cargo has two profiles. cargo build and cargo run use dev, which does not optimise and keeps debugging information (Cargo prints [unoptimized + debuginfo]); --release uses release, which optimises. They write to different folders, so switching does not overwrite the other build. cargo check leaves out the executable, which this program shows:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
// Runs `cargo <args>` in `dir` and prints what a terminal would show. Two things are
// trimmed: the build time ("in 0.06s") and the folder the project lives in.
fn cargo(dir: &Path, args: &[&str]) {
println!("$ cargo {}", args.join(" "));
let out = Command::new("cargo")
.args(args)
.current_dir(dir)
.output()
.unwrap();
let text =
String::from_utf8_lossy(&out.stderr).into_owned() + &String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
let line = line.replace(dir.parent().unwrap().to_str().unwrap(), "~/projects");
match line.find(" target(s) in ") {
Some(i) => println!("{} target(s)", &line[..i]),
None => println!("{line}"),
}
}
}
// The visible entries of a folder, folders marked with a slash.
fn ls(dir: &Path) -> String {
let mut names: Vec<String> = fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap())
.filter(|e| !e.file_name().to_string_lossy().starts_with('.'))
.map(|e| {
let slash = if e.path().is_dir() { "/" } else { "" };
format!("{}{slash}", e.file_name().to_string_lossy())
})
.collect();
names.sort();
names.join(" ")
}
fn main() {
let root = std::env::temp_dir().join("hw-profiles");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
cargo(&root, &["new", "hello", "-q"]);
let dir = root.join("hello");
fs::write(
dir.join("src/main.rs"),
"fn main() {\n println!(\"debug_assertions: {}\", cfg!(debug_assertions));\n}\n",
)
.unwrap();
cargo(&dir, &["check"]);
println!("project after check: {}", ls(&dir));
println!(
"target/debug after check: {}",
ls(&dir.join("target/debug"))
);
cargo(&dir, &["run"]);
cargo(&dir, &["run", "--release"]);
println!("target: {}", ls(&dir.join("target")));
println!("target/release: {}", ls(&dir.join("target/release")));
}
$ cargo run
$ cargo new hello -q
$ cargo check
    Checking hello v0.1.0 (~/projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s)
project after check: Cargo.lock  Cargo.toml  src/  target/
target/debug after check: build/  deps/  examples/  incremental/
$ cargo run
   Compiling hello v0.1.0 (~/projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s)
     Running `target/debug/hello`
debug_assertions: true
$ cargo run --release
   Compiling hello v0.1.0 (~/projects/hello)
    Finished `release` profile [optimized] target(s)
     Running `target/release/hello`
debug_assertions: false
target: CACHEDIR.TAG  debug/  release/
target/release: build/  deps/  examples/  hello  hello.d  incremental/

debug_assertions is on in dev and off in release, and debug_assert! uses it. Numbers and Types describes a setting that changes what a program does (integer overflow is a panic in dev and wraps in release); the Cargo book lists them all. Use dev while you write, and release for the build you measure or ship.

Going deeper

Rust’s standard output is line-buffered: text waits in memory until a newline arrives. The Stdout docs say so for a terminal, and the run below shows a pipe doing the same on this platform. A full line leaves at once. A partial line waits for a newline, a flush or a normal exit, and abort(), an exit that skips Rust’s normal cleanup, loses it. Each child below ends with abort() except normal-exit:

Rustsrc/main.rsRun ↗
use std::io::Write;
use std::process::Command;
// The program under test prints, then ends: with `abort()`, which skips normal cleanup,
// except in the `normal-exit` mode, which simply returns.
fn child(mode: &str) {
match mode {
"line" => println!("a line, with a newline"),
"partial" => print!("a partial line"),
"normal-exit" => return print!("a partial line, normal exit"),
_ => {
print!("a partial line, flushed");
std::io::stdout().flush().unwrap();
}
}
std::process::abort();
}
fn main() {
if let Some(mode) = std::env::args().nth(1) {
return child(&mode);
}
for mode in ["line", "partial", "flushed", "normal-exit"] {
let out = Command::new(std::env::current_exe().unwrap())
.arg(mode)
.output()
.unwrap();
println!(
"{mode}: stdout is {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
}
$ cargo run
line: stdout is "a line, with a newline\n"
partial: stdout is ""
flushed: stdout is "a partial line, flushed"
normal-exit: stdout is "a partial line, normal exit"

So a prompt written with print!("name: ") needs std::io::stdout().flush() before the program waits for input, or it may not appear in time. flush comes from the Write trait (use std::io::Write;).

Editions

edition = "2024" in Cargo.toml names a set of language rules. Editions let Rust change syntax without breaking old code: each package chooses its own, and cargo new writes the newest. rustc main.rs has no manifest to read, so it uses edition 2015 unless you pass --edition. The test below uses async, a keyword from edition 2018 on, as a variable name:

Rustsrc/main.rsRun ↗
use std::fs;
use std::process::Command;
// Runs a command in `dir` and prints its exit code and the first lines it said.
fn run(dir: &std::path::Path, program: &str, args: &[&str]) {
let out = Command::new(program)
.args(args)
.current_dir(dir)
.output()
.unwrap();
println!(
"$ {program} {} (exit code {:?})",
args.join(" "),
out.status.code()
);
let text =
String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr);
for line in text.lines().take(2) {
println!("{line}");
}
}
fn main() {
let dir = std::env::temp_dir().join("hw-rustc");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("main.rs"),
"fn main() {\n let async = 5;\n println!(\"async: {async}\");\n}\n",
)
.unwrap();
// No Cargo.toml here, so nothing tells rustc which edition to use.
run(&dir, "rustc", &["main.rs"]);
run(&dir, "./main", &[]);
run(&dir, "rustc", &["--edition", "2024", "main.rs"]);
}
$ cargo run
$ rustc main.rs  (exit code Some(0))
$ ./main   (exit code Some(0))
async: 5
$ rustc --edition 2024 main.rs  (exit code Some(1))
error: expected identifier, found keyword `async`
 --> main.rs:2:9

The Edition Guide lists what each edition changed.

Practice

Try it

Paste each program into the src/main.rs of a cargo new project and run cargo run until it prints all checks passed. Both start a copy of themselves, so they need a real project, not a sandbox.

An exit status

run is written and returns a Result. Write finish, which turns that result into what the process reports: on Ok(n) print n on standard output and succeed; on Err(message) print the message on standard error and exit with code 2. The checks start the program as a child and look at its exit code and both streams, including the failing inputs (code 2, nothing on standard output).

Starterexercises/exit-status.rs
use std::process::{Command, ExitCode};
/// The work, already written: halves a number given as text.
fn run(arg: Option<String>) -> Result<u32, String> {
match arg {
None => Err(String::from("missing number")),
Some(text) => match text.parse::<u32>() {
Ok(n) => Ok(n / 2),
Err(_) => Err(format!("not a number: {text}")),
},
}
}
/// Turns the outcome into what the process reports. `Ok(n)`: print `n` on standard
/// output and succeed. `Err(message)`: print the message on standard error and exit with
/// code 2, a usage error. (Returning the `Err` from `main` would give 1.)
fn finish(outcome: Result<u32, String>) -> ExitCode {
todo!("match on the Result; ExitCode::SUCCESS and ExitCode::from(2) are the two codes")
}
/// Runs this program again as `child <extra>` and returns its exit code, stdout, stderr.
fn child(extra: &[&str]) -> (Option<i32>, String, String) {
let out = Command::new(std::env::current_exe().unwrap())
.arg("child")
.args(extra)
.output()
.unwrap();
(
out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn main() -> ExitCode {
let mut args = std::env::args().skip(1);
if args.next().as_deref() == Some("child") {
return finish(run(args.next()));
}
assert_eq!(child(&["84"]), (Some(0), "42\n".into(), String::new()));
// The failing cases: code 2, and the message on standard error only.
assert_eq!(
child(&["abc"]),
(Some(2), String::new(), "not a number: abc\n".into())
);
assert_eq!(
child(&[]),
(Some(2), String::new(), "missing number\n".into())
);
println!("all checks passed");
ExitCode::SUCCESS
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/exit-status.solution.rsRun ↗
use std::process::{Command, ExitCode};
/// The work, already written: halves a number given as text.
fn run(arg: Option<String>) -> Result<u32, String> {
match arg {
None => Err(String::from("missing number")),
Some(text) => match text.parse::<u32>() {
Ok(n) => Ok(n / 2),
Err(_) => Err(format!("not a number: {text}")),
},
}
}
/// Turns the outcome into what the process reports. `Ok(n)`: print `n` on standard
/// output and succeed. `Err(message)`: print the message on standard error and exit with
/// code 2, a usage error. (Returning the `Err` from `main` would give 1.)
fn finish(outcome: Result<u32, String>) -> ExitCode {
match outcome {
Ok(n) => {
println!("{n}");
ExitCode::SUCCESS
}
Err(message) => {
eprintln!("{message}");
ExitCode::from(2)
}
}
}
/// Runs this program again as `child <extra>` and returns its exit code, stdout, stderr.
fn child(extra: &[&str]) -> (Option<i32>, String, String) {
let out = Command::new(std::env::current_exe().unwrap())
.arg("child")
.args(extra)
.output()
.unwrap();
(
out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn main() -> ExitCode {
let mut args = std::env::args().skip(1);
if args.next().as_deref() == Some("child") {
return finish(run(args.next()));
}
assert_eq!(child(&["84"]), (Some(0), "42\n".into(), String::new()));
// The failing cases: code 2, and the message on standard error only.
assert_eq!(
child(&["abc"]),
(Some(2), String::new(), "not a number: abc\n".into())
);
assert_eq!(
child(&[]),
(Some(2), String::new(), "missing number\n".into())
);
println!("all checks passed");
ExitCode::SUCCESS
}
$ cargo run
all checks passed

The wrong stream

This program prints a warning on standard output, mixed in with its answer, so cmd > result.txt would capture the warning. Move it to the right stream and make the checks pass.

Starterexercises/wrong-stream.rs
use std::process::Command;
/// The program under test. Its answer belongs on standard output; its warning is a
/// message about the run, not part of the answer, so it belongs on standard error.
fn child() {
println!("warning: no config file, using defaults");
let answer = 6 * 7;
println!("result: {answer}");
}
// Runs `child` as a separate process so that the two streams can be told apart.
fn main() {
if std::env::args().nth(1).as_deref() == Some("child") {
return child();
}
let out = Command::new(std::env::current_exe().unwrap())
.arg("child")
.output()
.unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), "result: 42\n");
assert_eq!(
String::from_utf8_lossy(&out.stderr),
"warning: no config file, using defaults\n"
);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/wrong-stream.solution.rsRun ↗
use std::process::Command;
/// The program under test. Its answer belongs on standard output; its warning is a
/// message about the run, not part of the answer, so it belongs on standard error.
fn child() {
eprintln!("warning: no config file, using defaults");
let answer = 6 * 7;
println!("result: {answer}");
}
// Runs `child` as a separate process so that the two streams can be told apart.
fn main() {
if std::env::args().nth(1).as_deref() == Some("child") {
return child();
}
let out = Command::new(std::env::current_exe().unwrap())
.arg("child")
.output()
.unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), "result: 42\n");
assert_eq!(
String::from_utf8_lossy(&out.stderr),
"warning: no config file, using defaults\n"
);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more