Working with Environment Variables
Many CLI programs allow configuration through environment variables. Let's add case-insensitive search support using OXGREP_IGNORE_CASE.
Reading Environment Variables
The std.env module provides functions to read environment variables:
import std.env
fn main() {
// Get a variable - returns String?
let debugMode = std.env.var("DEBUG")
match debugMode {
Ok(value) -> println!("DEBUG is set to: \(value)"),
Err(_) -> println!("DEBUG is not set"),
}
}
Checking for a Flag
To check if an environment variable exists, just see if the result is Ok:
import std.env
fn main() {
let ignoreCase = std.env.var("OXGREP_IGNORE_CASE").isOk()
if ignoreCase {
println!("Case-insensitive search enabled")
}
}
Updating Our Config Struct
Let's integrate environment variable support into our Config struct:
import std.env
struct Config {
query: String,
filename: String,
ignoreCase: Bool,
}
extension Config {
static fn new(args: &Vec<String>): Result<Config, String> {
if args.len() < 3 {
return Err(
"not enough arguments\nusage: oxgrep <query> <filename>".toString()
)
}
let query = args[1].clone()
let filename = args[2].clone()
// Check both command-line flag and environment variable
let commandLineFlag = args.len() > 3 && args[3] == "--ignore-case"
let envVariable = std.env.var("OXGREP_IGNORE_CASE").isOk()
let ignoreCase = commandLineFlag || envVariable
Ok(Config {
query,
filename,
ignoreCase,
})
}
}
Implementing Case-Insensitive Search
Now update the search function to use the ignoreCase field:
fn search(config: &Config, contents: &str): Vec<String> {
var results = Vec.new()
for line in contents.lines() {
if config.ignoreCase {
if line.toLowercase().contains(&config.query.toLowercase()) {
results.push(line.toString())
}
} else {
if line.contains(&config.query) {
results.push(line.toString())
}
}
}
results
}
Or more concisely:
fn search(config: &Config, contents: &str): Vec<String> {
var results = Vec.new()
let query = if config.ignoreCase {
config.query.toLowercase()
} else {
config.query.clone()
}
for line in contents.lines() {
let searchLine = if config.ignoreCase {
line.toLowercase()
} else {
line.toString()
}
if searchLine.contains(&query) {
results.push(line.toString())
}
}
results
}
Updated Main Function
Update main and run to pass the config around:
import std.fs
import std.env
import std.error.Error
fn main(): Result<(), Box<dyn Error>> {
let args = std.env.args().collect<Vec<String>>()
let config = Config.new(&args)
.unwrapOrElse { err ->
eprintln!("Problem parsing arguments: {}", err)
std.process.exit(1)
}?
run(&config)
}
fn run(config: &Config): Result<(), Box<dyn Error>> {
let contents = std.fs.readToString(&config.filename)?
let results = search(config, &contents)
for line in results {
println!("{}", line)
}
Ok(())
}
Testing Environment Variable Behavior
You can test environment variable behavior from the command line:
# Case-sensitive (default)
cargo run -- "is" poem.txt
# Output: Lines containing "is" (lowercase)
# Case-insensitive via environment variable
OXGREP_IGNORE_CASE=1 cargo run -- "is" poem.txt
# Output: Lines containing "is" or "IS" or "Is"
# Case-insensitive via command-line flag
cargo run -- "is" poem.txt --ignore-case
# Output: Same as above
Getting Multiple Values
For more complex configuration, you might read multiple environment variables:
import std.env
struct AppConfig {
query: String,
filename: String,
ignoreCase: Bool,
maxResults: UInt,
verbose: Bool,
}
extension AppConfig {
static fn fromEnv(): Self {
let ignoreCase = std.env.var("OXGREP_IGNORE_CASE").isOk()
let maxResults = std.env.var("OXGREP_MAX_RESULTS")
.ok()
.andThen { s -> s.parse<UInt>().ok() }
.unwrapOr(1000)
let verbose = std.env.var("OXGREP_VERBOSE").isOk()
AppConfig {
query: String.new(),
filename: String.new(),
ignoreCase,
maxResults,
verbose,
}
}
}
Environment Variables in Tests
When writing tests, you can set environment variables programmatically:
#[test]
fn testIgnoreCaseFromEnv() {
// In real tests, you'd need to set the environment before creating Config
// This is more complex due to test concurrency
}
Important Notes About Environment Variables
-
Thread Safety - Reading environment variables is thread-safe, but setting them is not. Set variables before spawning threads.
-
Performance - Environment variable lookups are relatively expensive. Cache values if you use them frequently.
-
Naming Conventions - Use UPPERCASE_WITH_UNDERSCORES for environment variable names.
-
Documentation - Always document which environment variables your program recognizes.
-
Security - Be careful with sensitive data in environment variables (passwords, API keys). They're visible in process listings.
Summary
Environment variables allow flexible configuration:
std.env.var- Read a variable, returnsResult<String, VarError>- Defaults - Use
unwrapOrto provide defaults - Combining sources - Command-line flags and env vars work together
- Caching - Read environment variables early, not in loops
- Testing - Be aware that environment variable state is global
Next, we'll write comprehensive tests for our program.