491: Path and PathBuf Handling
Difficulty: 1 Level: Beginner Platform-aware filesystem paths โ like `String`/`&str` but for paths, with OS separator handling.The Problem This Solves
In Python and JavaScript, file paths are plain strings. `os.path.join("/home/user", "file.txt")` works on Unix but breaks on Windows if you manually concatenate with `/`. The language doesn't enforce the distinction between "a string" and "a path." Rust has dedicated types: `Path` (borrowed, like `&str`) and `PathBuf` (owned, like `String`). They know about platform path separators โ `/` on Unix, `\` on Windows. Calling `.join()`, `.parent()`, `.file_name()`, `.extension()` โ these work correctly on all platforms without you thinking about it. More importantly, using `Path`/`PathBuf` in function signatures makes your API self-documenting. When a function takes `&Path`, it's clear it expects a filesystem path, not an arbitrary string. You also get protection from path traversal issues โ constructing paths with `.join()` won't accidentally interpret `..` as you'd expect with string concatenation, and you can check `.is_absolute()`.The Intuition
`Path` is to `PathBuf` as `&str` is to `String`. `Path` is a borrowed reference to path data โ you pass it around cheaply. `PathBuf` is the owned, growable version you can build up incrementally. The mental model:- Start with `PathBuf::from("/base")` when you own a path
- Use `path.push("component")` to append segments
- Use `path.join("component")` to get a new path without modifying the original
- Accept `&Path` in function parameters (most flexible โ both `Path` and `PathBuf` coerce to it)
How It Works in Rust
use std::path::{Path, PathBuf};
// PathBuf โ owned, growable
let mut p = PathBuf::from("/home");
p.push("user"); // /home/user
p.push("docs"); // /home/user/docs
p.push("file.txt"); // /home/user/docs/file.txt
// Path โ borrowed view, decompose it
let path = Path::new("/home/user/docs/file.txt");
path.parent() // Some("/home/user/docs")
path.file_name() // Some("file.txt")
path.file_stem() // Some("file") โ without extension
path.extension() // Some("txt")
path.is_absolute() // true
// Components โ iterate path segments
for component in path.components() {
println!("{:?}", component);
}
// RootDir, Normal("home"), Normal("user"), Normal("docs"), Normal("file.txt")
// join โ builds new PathBuf (non-mutating)
let base = Path::new("/home/user");
let full = base.join("projects").join("rust").join("main.rs");
// /home/user/projects/rust/main.rs
// with_extension โ change or add extension
Path::new("report.txt").with_extension("pdf"); // report.pdf
Path::new("archive").with_extension("tar.gz"); // archive.tar.gz
// starts_with / ends_with โ path prefix/suffix check
path.starts_with("/home") // true
path.ends_with("file.txt") // true
// Accept &Path in functions โ both Path and PathBuf coerce
fn print_ext(p: &Path) {
if let Some(ext) = p.extension() {
println!("{}", ext.to_string_lossy());
}
}
print_ext(path); // works with &Path
print_ext(&p); // works with &PathBuf too (auto-coerce)
What This Unlocks
- Cross-platform path handling โ `.join()` uses the right separator automatically on all platforms.
- Safe path decomposition โ extract filename, stem, extension, parent without string-splitting manually.
- Type-safe APIs โ functions taking `&Path` can't accidentally receive a non-path string.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Path type | `string` (no dedicated type) | `Path` (borrowed) / `PathBuf` (owned) |
| Join path segments | `String.concat "/" parts` | `path.join("segment")` |
| Get filename | Manual `String.rindex_opt` | `path.file_name()` โ `Option<&OsStr>` |
| Get extension | Manual `String.rindex_opt '.'` | `path.extension()` โ `Option<&OsStr>` |
| Get parent dir | Manual split | `path.parent()` โ `Option<&Path>` |
| Platform separators | Manual | Automatic โ `/` on Unix, `\` on Windows |
| Append segment | String concat | `pathbuf.push("segment")` (mutates) |
// 491. Path and PathBuf handling
use std::path::{Path, PathBuf};
fn main() {
// PathBuf โ owned, growable
let mut p = PathBuf::from("/home");
p.push("user");
p.push("documents");
p.push("file.txt");
println!("path: {}", p.display());
// Path โ borrowed view
let path = Path::new("/home/user/documents/file.txt");
println!("parent: {:?}", path.parent());
println!("file_name: {:?}", path.file_name());
println!("stem: {:?}", path.file_stem());
println!("extension: {:?}", path.extension());
println!("is_abs: {}", path.is_absolute());
// Components
for comp in path.components() { println!(" {:?}", comp); }
// Join
let base = Path::new("/home/user");
let full = base.join("projects").join("rust").join("main.rs");
println!("joined: {}", full.display());
// Absolute path from relative
let rel = Path::new("src/main.rs");
println!("rel parent: {:?}", rel.parent());
// with_extension
let p2 = Path::new("file.txt");
let rs = p2.with_extension("rs");
println!("with_ext: {}", rs.display());
// starts_with
println!("starts /home: {}", path.starts_with("/home"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_join() { let p=PathBuf::from("/a").join("b").join("c"); assert_eq!(p.to_str().unwrap(),"/a/b/c"); }
#[test] fn test_ext() { assert_eq!(Path::new("f.txt").extension().unwrap(),"txt"); }
#[test] fn test_stem() { assert_eq!(Path::new("f.txt").file_stem().unwrap(),"f"); }
#[test] fn test_parent() { assert_eq!(Path::new("/a/b/c").parent().unwrap(),Path::new("/a/b")); }
}
(* 491. Path handling โ OCaml *)
let () =
let path = "/home/user/documents/file.txt" in
let parts = String.split_on_char '/' path |> List.filter ((<>) "") in
Printf.printf "parts: %s\n" (String.concat " | " parts);
let dir = String.concat "/" ("" :: List.rev (List.tl (List.rev parts))) in
Printf.printf "dir: %s\n" dir;
let base = List.nth parts (List.length parts - 1) in
Printf.printf "file: %s\n" base;
let ext = match String.rindex_opt base '.' with
| Some i -> Some (String.sub base (i+1) (String.length base - i - 1))
| None -> None
in
Printf.printf "ext: %s\n" (Option.value ~default:"none" ext)