Project: File Processor
This chapter is the capstone of Phase 2. We will build a complete CLI file processor -- a small tool that reads a text file, searches for lines matching a pattern, and reports the results. Along the way, we will put together every concept from Phase 2: custom types, enums, error handling with !T, file I/O, and generics.
Our file processor will combine:
- Custom types (Chapter 10) -- a
Configtype for program settings - Enums and pattern matching (Chapter 11) -- modeling outcomes
- Error handling with
!T(Chapter 12) -- propagating and handling errors with the bang type - File I/O (Chapter 12) -- reading file contents as strings
- Generics (Chapter 13) -- writing reusable search with type parameters and higher-order functions
Setting Up
Here is what we are building: a command-line tool called fileproc that accepts an input file path, an output file path, and a verbose flag. It reads the input file, searches each line for the word "hello", counts the matches, and optionally prints each matching line.
By the end of this chapter, the program will:
- Parse command-line arguments into a
Configtype - Validate the configuration, rejecting invalid inputs
- Read file contents with proper error handling
- Search lines using a generic, higher-order function
- Print results with optional verbose output
We will build this step by step, adding one concept at a time.
Parsing Arguments
The first step in any CLI program is gathering configuration from the user. Rather than scattering individual variables throughout the code, we group all settings into a single Config type. This makes the code easier to read, easier to pass around, and easier to extend later.
This prints:
Input: input.txt
Output: output.txtThe Config type has three fields: two strings for file paths and a boolean for verbose mode. The parse_args function constructs a Config with default values. In a real program, this function would read env.args() to get command-line arguments -- for now we use hardcoded defaults to keep things simple.
Accessing fields uses dot notation: config.input_path, config.verbose. In Rust, this maps directly to struct field access. In C, it is the same with struct pointers. In Python and TypeScript, these become class attributes.
This pattern -- grouping related values into a type -- is one of the most important organizational tools in Auto. As the program grows, you can add new fields to Config without changing how it is passed around.
Reading Files
Once we have a config, we need to read the file. File operations can fail: the file might not exist, the user might lack permissions, or the disk might be full. Auto handles this with the !T (Result) type.
This prints:
Hello from file!The read_file function returns !String, which is Auto's Result type. The ! prefix means "this operation might fail." On success, it returns the file contents as a String. On failure, it returns an error message.
The caller unwraps the result with ! (the postfix bang operator, equivalent to Rust's ?). If read_file fails, the error is propagated up immediately. If it succeeds, content holds the file contents as a plain String.
This is the same pattern we saw in Chapter 12. The key difference from languages like Python or JavaScript is that Auto forces you to acknowledge the possibility of failure. You cannot accidentally ignore an error -- the compiler requires the ! unwrap.
In C, the transpiler generates a tagged union (ResultString) with an is_ok flag. The caller must check the flag before accessing the value. This is exactly what Auto does internally, but the compiler handles it automatically.
Validating Configuration
Before processing, we should validate the config. An empty input path makes no sense -- we should reject it early with a clear error message. This is where !T really shines: validation errors propagate through the same mechanism as I/O errors.
This prints:
data.txtThe validate function returns !Config -- either the same config (if valid) or an error string (if invalid). In Auto, the last expression in a function body is the return value. The if/else expression returns "input path cannot be empty" on the error branch and c (the original config) on the success branch.
This is a powerful pattern. The return type !Config means the function can either succeed with a Config or fail with a String. The caller uses ! to unwrap, propagating any error. Because both validate and read_file return !T, they chain naturally: each ! in the call stack propagates errors upward until main handles them.
How this maps across languages:
- Rust uses
Result<Config, String>withOk(c)andErr(msg). The?operator unwraps and propagates errors. - Python raises a
ValidationErrorexception. There is no compile-time requirement to handle it. - C uses a tagged union
ResultConfigwith manualis_okchecking. - TypeScript throws an
Error. Like Python, error handling is optional.
Generic Search
Now we arrive at the core of our processor: searching lines. We want a reusable function that takes a list of strings and a predicate (a function that decides whether a line matches), and returns the matching results.
This is a perfect use case for generics combined with higher-order functions. The search logic is the same regardless of what the predicate returns -- it could return a boolean, a filtered string, a count, or anything else.
This prints:
hello world
hello autoLet us break down the key parts:
fn search_lines<T>-- The function is generic over typeT. The predicate decides whatTis.predicate fn(String) T-- The second parameter is itself a function. It takes aStringand returns aT. This is a higher-order function -- a function that takes another function as an argument.Anonymous function -- The predicate is defined inline:
fn(s String) ?String { ... }. This is Auto's syntax for anonymous functions (similar to Rust closures or JavaScript arrow functions). The return type?Stringmeans it returns either a matching string ornone.s.contains("hello")-- String method that checks if the substring exists.
The predicate returns ?String (Option). When the line contains "hello", it returns s (the line itself). Otherwise it returns none. The search_lines function collects all non-none results into the output list.
This design is deeply flexible. You could pass a different predicate to count matches, extract email addresses, find lines longer than 80 characters, or anything else -- all without changing search_lines. The generic type parameter T ensures that the function works with any return type the predicate produces.
In Rust, this maps to closures (|s: &String| -> Option<String>). In Python, it is a lambda. In TypeScript, an arrow function. In C, the transpiler generates a concrete function per predicate because C has no closures.
The Complete Processor
Now we bring everything together. The complete file processor combines the Config type, file reading with error handling, line iteration, and verbose output into a single, clean program.
This prints:
Found: hello world
Found: hello auto
Found 2 matchesLet us trace through the execution:
maincreates aConfigwithverbose: trueand callsprocess.processcallsread_file, which returns simulated file content. The!unwrap propagates any error.content.split("\n")breaks the string into an array of lines.- The
forloop checks each line. Whenline.contains("hello")is true, it incrementscountand prints the line (becauseverboseis true). - The function returns
count, whichmainprints as the final summary.
Notice how clean the error handling is. Both read_file and process return !T. The ! operator at each call site propagates errors upward. If main wanted to handle errors itself, it could use an is expression instead of !:
is read_file("data.txt") {
Ok(content) => print(content),
Err(msg) => eprint("Error: " + msg)
}This is the same Result pattern from Chapter 12, now applied in a real project context.
Practical File I/O Patterns
The file processor above demonstrated structured file processing with error handling. Here are four common file I/O patterns you will encounter in practice.
Reading Lines from a File
The most basic file operation: write content, read it back, and iterate over lines.
Counting Lines in a Temporary File
A common pattern for testing: create a temporary file, process it, then clean up.
Reading CSV Data
CSV (Comma-Separated Values) is the simplest structured data format. In Auto, you can parse it with split on newlines and commas.
Filtering CSV Data
Combining CSV parsing with conditional logic lets you filter rows based on field values.
These four patterns cover the most common file I/O scenarios:
- Read lines -- The foundation: write, read, split, iterate.
- Temp files -- Create-process-delete is the standard testing pattern.
- CSV parsing -- Split on newlines then commas to extract fields.
- CSV filtering -- Combine parsing with conditional logic for data queries.
Summary
This capstone project brought together every concept from Phase 2:
| Concept | Where We Used It |
|---|---|
| Custom types | Config type groups all settings |
| Field access | config.input_path, config.verbose |
Error handling (!T) | read_file and process return !T |
| Error propagation | ! unwrap at each call site |
| String methods | s.contains("hello"), content.split("\n") |
| Control flow | if/else for validation and verbose output |
| For loops | Iterating over lines |
| Generics | search_lines<T> with type parameter |
| Higher-order functions | predicate fn(String) T parameter |
| Anonymous functions | Inline fn(s String) ?String { ... } |
Option type (?T) | Predicate returns ?String for filtering |
Here is what we learned across Phase 2:
Types (Chapter 10) give us structured data with named fields.
Configis the backbone of our program.Enums (Chapter 11) model data that can be one of several variants. The
?T(Option) and!T(Result) types are enums under the hood.Error handling (Chapter 12) uses
!Tto make failure explicit. The!unwrap operator propagates errors cleanly. There are no surprise crashes.Generics (Chapter 13) let us write functions that work across types.
search_lines<T>is reusable with any predicate return type.
What is coming in Phase 3:
- Closures and iterators -- more powerful ways to process collections
- Smart pointers -- owned references, interior mutability
- Concurrency -- writing safe parallel code
- Advanced type system features -- lifetimes, trait objects, and more
You now have the tools to build real programs in Auto. The file processor may be small, but it demonstrates the same patterns used in production codebases: structured types, explicit error handling, and reusable generic functions.