Systems Basics
10 examples for processes, files, and OS interaction using Rust's standard library.
Prerequisites
- Rust 1.97.0 (edition 2024)
- Familiarity with ownership and
Result-based error handling
Basic Examples
1. Reading a File
use std::fs;
fn main() -> anyhow::Result<()> {
let contents = fs::read_to_string("config.toml")?;
println!("{contents}");
Ok(())
}fs::read_to_stringhandles UTF-8 decoding- Errors include the OS error code via
std::io::Error - Use
readfor binary data - Prefer
tokio::fsin async applications
2. Writing a File Atomically
use std::fs;
fn write_atomically(path: &str, data: &str) -> std::io::Result<()> {
let tmp = format!("{path}.tmp");
fs::write(&tmp, data)?;
fs::rename(&tmp, path)
}- Write to a temp file, then rename for atomic replace
renameis atomic on the same filesystem- Crash mid-write leaves the original intact
- Common pattern for config and state files
3. Spawning a Child Process
use std::process::Command;
fn main() -> anyhow::Result<()> {
let output = Command::new("git")
.args(["status", "--short"])
.output()?;
println!("{}", String::from_utf8_lossy(&output.stdout));
Ok(())
}output()waits for completion and captures streamsspawn()returns aChildfor background processes- Set
current_dirandenvon the builder - Check
status.success()before trusting stdout
4. Environment Variables
use std::env;
fn main() {
for (key, value) in env::vars() {
if key.starts_with("APP_") {
println!("{key}={value}");
}
}
}env::varreturnsResultfor a single variableset_varis unsafe in multithreaded programs (Rust 2024 documents this)- Child processes inherit the parent environment
- Use typed config loading for production apps
5. File Metadata
use std::fs;
fn main() -> anyhow::Result<()> {
let meta = fs::metadata("Cargo.toml")?;
println!("size: {} bytes", meta.len());
println!("is_dir: {}", meta.is_dir());
Ok(())
}metadatafollows symlinks;symlink_metadatadoes not- Check
permissions()on Unix for mode bits modified()returns filesystem timestamps- Handle missing files with
?ormatch
6. Directory Traversal
use std::fs;
use std::path::Path;
fn list_rs(dir: &Path) -> walkdir::Result<()> {
for entry in walkdir::WalkDir::new(dir) {
let entry = entry?;
if entry.path().extension().is_some_and(|e| e == "rs") {
println!("{}", entry.path().display());
}
}
Ok(())
}walkdircrate simplifies recursive walks- Skip permission errors with
WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) - Respect
.gitignorewith theignorecrate for tooling - Avoid unbounded recursion on cyclic symlinks
7. TCP Socket (std)
use std::io::{Read, Write};
use std::net::TcpStream;
fn main() -> anyhow::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:8080")?;
stream.write_all(b"GET /health HTTP/1.0\r\n\r\n")?;
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf)?;
println!("{}", String::from_utf8_lossy(&buf[..n]));
Ok(())
}- Blocking I/O suits simple tools and scripts
- Use
TcpListenerfor servers - Production servers prefer
tokio::net - Set
read_timeout/write_timeoutfor resilience
Intermediate Examples
8. Process Pipes
use std::process::{Command, Stdio};
fn main() -> anyhow::Result<()> {
let mut child = Command::new("grep")
.arg("error")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(b"log line\nerror: bad\n")?;
let output = child.wait_with_output()?;
println!("{}", String::from_utf8_lossy(&output.stdout));
Ok(())
}- Pipe stdin/stdout between parent and child
- Always
waitorwait_with_outputto reap zombies Stdio::null()discards streams- Foundation for shell-like tools
Related: Signals & Process Control
9. Unix Permissions
#[cfg(unix)]
fn make_executable(path: &str) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms)?;
Ok(())
}- Platform-specific extensions live under
std::os - Use
cfg(unix)/cfg(windows)for portability - Document permission expectations in CLI help
- See cross-platform page for abstraction patterns
Related: Cross-Platform Systems Code
10. Temporary Files
use tempfile::NamedTempFile;
fn main() -> anyhow::Result<()> {
let mut tmp = NamedTempFile::new()?;
std::io::Write::write_all(&mut tmp, b"scratch data")?;
let path = tmp.path().to_owned();
// use path; file deleted when `tmp` drops
Ok(())
}tempfilecrate handles secure temp paths- Files auto-delete on drop unless persisted
- Use
tempdir()for scratch directories - Prefer temp files over
/tmp/myapp-$$hand-rolling
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.