Reading and Writing Files
In Oxide (like Rust), working with the file system requires the std.fs module. This section covers reading files, a fundamental operation for our grep-like program.
Reading a File
Let's start with the most basic operation: reading a file's contents into a string.
Create a sample file named poem.txt:
I'm nobody! Who are you?
Are you nobody too?
Then there's a pair of us!
Don't tell! they'd banish us, you know.
How dreary to be somebody!
How public, like a Frog
To tell one's name the livelong June
To an admiring Bog!
Now, let's write code to read this file. Update src/main.ox:
import std.fs
fn main() {
let filename = "poem.txt"
let contents = std.fs.readToString(filename)
println!("File contents:\n\(contents)")
}
Run the program:
cargo run
Output:
File contents:
I'm nobody! Who are you?
Are you nobody too?
...
Handling Errors with Result
What happens if the file doesn't exist? The readToString function returns a Result<String, Error>, which means it can fail. We need to handle this using the ? operator.
import std.fs
fn main() {
let filename = "poem.txt"
let contents = std.fs.readToString(filename)?
println!("File contents:\n\(contents)")
}
But wait—main doesn't return Result, it returns nothing. We need to change that:
import std.fs
fn main(): Result<(), Box<dyn Error>> {
let filename = "poem.txt"
let contents = std.fs.readToString(filename)?
println!("File contents:\n\(contents)")
Ok(())
}
Now if the file doesn't exist, the error message will be printed by Rust's error handling system:
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Using Variables from Arguments
In a real program, the filename shouldn't be hardcoded. Let's accept it as a command-line argument.
First, let's use std.env to get command-line arguments:
import std.fs
import std.env
import std.error.Error
fn main(): Result<(), Box<dyn Error>> {
let args = std.env.args().collect<Vec<String>>()
if args.len() < 2 {
eprintln!("usage: oxgrep <query> <filename>")
std.process.exit(1)
}
let query = args[1].clone()
let filename = args[2].clone()
let contents = std.fs.readToString(&filename)?
println!("In file \(filename)")
println!("Contents: \(contents)")
Ok(())
}
Now you can run:
cargo run -- to poem.txt
Output:
In file poem.txt
Contents:
I'm nobody! Who are you?
...
Processing File Contents
Now that we can read files, let's search for matching lines. We'll create a function to do the heavy lifting:
import std.fs
import std.env
import std.error.Error
fn search(query: &str, contents: &str): Vec<String> {
var results = Vec.new()
for line in contents.lines() {
if line.contains(query) {
results.push(line.toString())
}
}
results
}
fn main(): Result<(), Box<dyn Error>> {
let args = std.env.args().collect<Vec<String>>()
if args.len() < 3 {
eprintln!("usage: oxgrep <query> <filename>")
std.process.exit(1)
}
let query = args[1].clone()
let filename = args[2].clone()
let contents = std.fs.readToString(&filename)?
let results = search(&query, &contents)
for line in results {
println!("{}", line)
}
Ok(())
}
Let's test it:
cargo run -- is poem.txt
Output:
I'm nobody! Who are you?
Are you nobody too?
Then there's a pair of us!
How public, like a Frog
Perfect! But this approach is inefficient for large files because it stores all matching lines in memory. For a real grep tool, we'd print lines as we find them. However, for learning purposes, this demonstrates the concept clearly.
Improving Efficiency
For large files, a better approach is to print directly without storing results:
import std.fs
import std.env
import std.error.Error
fn main(): Result<(), Box<dyn Error>> {
let args = std.env.args().collect<Vec<String>>()
if args.len() < 3 {
eprintln!("usage: oxgrep <query> <filename>")
std.process.exit(1)
}
let query = args[1].clone()
let filename = args[2].clone()
let contents = std.fs.readToString(&filename)?
for line in contents.lines() {
if line.contains(&query) {
println!("{}", line)
}
}
Ok(())
}
This version is simpler and more memory-efficient—perfect for real applications.
Writing Files
While our grep tool only reads files, it's useful to know how to write files. Use std.fs.write:
import std.fs
fn main(): Result<(), Box<dyn Error>> {
let content = "Hello, World!"
std.fs.write("output.txt", content)?
println!("File written successfully")
Ok(())
}
For more complex file operations (appending, seeking), use OpenOptions:
import std.fs
fn main(): Result<(), Box<dyn Error>> {
// Append to a file
var file = std.fs.OpenOptions.new()
.append(true)
.open("log.txt")?
std.io.Write.writeAll(&mut file, b"Log entry\n")?
Ok(())
}
Summary
File I/O in Oxide follows Rust's patterns:
std.fs.readToString- Read an entire file into aStringstd.fs.write- Write content to a filestd.fs.OpenOptions- For more control over how files are opened- Result-based errors - Always handle potential failures with
?ormatch - Iterating over lines - Use
.lines()to process files line-by-line
Next, we'll make our program more user-friendly by handling command-line arguments better.