Project: Building a Command Line Program (Rust)
Reference: The Rust Programming Language, Chapter 12
Overview
This project develops a simplified Rust command-line program through six structured labs. The program
reads command-line arguments, reads a file, searches for text, handles errors properly, supports
environment variables, and follows good software engineering practices such as separation of concerns
and test-driven development (TDD).
Lab 1: Using Vectors and Strings
Objective
Use iterators to collect command-line arguments into a vector.
Introduction
• Command-line arguments are values passed to a program when it is executed.
• Vectors ( Vec<T> ) are growable arrays in Rust.
• Iterators allow sequential access to collection elements.
• Rust provides std::env::args() to access command-line arguments as an iterator of String .
Method
1. Documentation
The program collects command-line arguments into a vector and accesses individual elements.
Usage:
cargo run arg1 arg2
2. Code Listing ([Link])
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
1
println!("Program name: {}", args[0]);
if [Link]() > 1 {
println!("First argument: {}", args[1]);
}
}
3. Testing and Results
• Running cargo run hello prints the program name and hello .
4. Analysis, Discussion, and Conclusion
This lab demonstrates how iterators and vectors simplify handling command-line input. Accessing
arguments via indexing is straightforward but requires length checks to avoid runtime errors.
Lab 2: Reading a File
Objective
Read and display the contents of a file using command-line arguments.
Introduction
Rust reads files using the std::fs module. File operations return Result<T, E> for error handling.
Method
1. Documentation
The program reads a filename passed as a command-line argument and prints its contents.
2. Code Listing
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let filename = &args[1];
let contents = fs::read_to_string(filename)
.expect("Could not read file");
2
println!("{}", contents);
}
3. Testing and Results
• Valid file: contents printed.
• Invalid file: program panics with an error message.
4. Analysis, Discussion, and Conclusion
Rust’s Result type enforces error awareness. Using expect is simple but not ideal for production
programs.
Lab 3: Organising Code in Modules
Objective
Refactor code into modules using separation of concerns.
Introduction
• Modules organize code into logical units.
• Separation of concerns ensures each part of the program has a single responsibility.
Method
[Link] (Program Control)
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
3
}
}
[Link] (Logic)
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if [Link]() < 3 {
return Err("Not enough arguments");
}
Ok(Config {
query: args[1].clone(),
filename: args[2].clone(),
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string([Link])?;
println!("{}", contents);
Ok(())
}
Analysis and Conclusion
Separating logic improves readability, maintainability, and testability. [Link] handles execution flow,
while [Link] contains reusable logic.
Lab 4: Test Driven Development (TDD)
Objective
Search for a word in a file and return matching lines.
4
Introduction
TDD involves writing failing tests before implementing functionality, ensuring correctness and
maintainability.
Method
1. Failing Test
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "Rust:\nsafe, fast, productive.\nPick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
2. Code to Pass the Test
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in [Link]() {
if [Link](query) {
[Link](line);
}
}
results
}
Borrowing and Lifetimes
Returned lines borrow from contents , avoiding unnecessary allocations. The lifetime 'a ensures safety.
5
Lab 5: Environment Variables
Objective
Support case-insensitive search using environment variables.
Introduction
Environment variables allow configuring program behavior externally. Rust accesses them via
std::env::var .
Method
use std::env;
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a
str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in [Link]() {
if line.to_lowercase().contains(&query) {
[Link](line);
}
}
results
}
let case_insensitive = env::var("CASE_INSENSITIVE").is_ok();
Lab 6: Writing Error Messages to Standard Error
Objective
Direct error messages to the standard error stream.
Introduction
• stdout is for normal output.
• stderr is for errors.
• Rust uses eprintln! for standard error.
6
Method
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
Testing and Results
• Redirecting output still displays errors on the terminal.
Final Conclusion
This project demonstrates core Rust concepts including ownership, error handling, modular design, testing,
and environment configuration. Following Chapter 12 of The Rust Book, the labs progressively build a robust
and maintainable command-line application.