How to join paths without breaking on Windows
Short answer: Build paths with PathBuf::push and join — they insert the platform’s separator (/ on Unix, \ on Windows) only when it’s needed, and never leave you with logs//app.txt. String concatenation bakes a separator in by hand, and that’s where a path that works on your machine breaks somewhere else.
What concatenation gets wrong
Section titled “What concatenation gets wrong”From a fresh project — cargo new join-demo, replace the contents of src/main.rs with the block below, and cargo run. Nothing here touches the filesystem, so the output is the same from any directory:
use std::path::PathBuf;
fn main() { let dir = PathBuf::from("logs/"); let name = "app.txt"; let by_hand = format!("{}/{}", dir.display(), name); println!("by hand: {}", by_hand); println!("join: {}", dir.join(name).display());}by hand: logs//app.txtjoin: logs/app.txtPath doesn’t normalise anything. The doubled slash is in the string you built, and it’s what your program prints, logs, and hands to the OS. Trim the trailing slash off the base and you get the opposite bug: forget the / in the format string and logs plus app.txt becomes logsapp.txt. Every concatenation site has to know whether its left side already ends in a separator. join doesn’t, because it checks.
What push and join do instead
Section titled “What push and join do instead”use std::path::{Path, PathBuf};
fn main() { let mut path = PathBuf::from("logs"); path.push("app.txt"); println!("push: {}", path.display());
let base = Path::new("/var/log"); let supplied = "/etc/hosts"; println!("join abs: {}", base.join(supplied).display());}push: logs/app.txtjoin abs: /etc/hostsTwo behaviours worth knowing. First, push adds a separator only when the current path doesn’t already end in one, so a base of logs/ and a base of logs both land on logs/app.txt. Second, the one-line rule the std docs give for push and join alike: if path is absolute, it replaces the current path. That’s why /var/log joined with /etc/hosts comes out as just /etc/hosts — sometimes what you want, when a user-supplied absolute path should win, and sometimes a silent discard of your base directory because a variable happened to start with /. The docs spell out the extra Windows cases (a path with a root but no drive prefix, like \windows, replaces everything except the base’s prefix), which is one more reason to let push make the call instead of assembling the string yourself.
The Windows part
Section titled “The Windows part”None of the code above types a separator character. On this machine (macOS, Rust 1.99.0) the one std uses is:
use std::path;
fn main() { println!("this platform's separator is {}", path::MAIN_SEPARATOR);}this platform's separator is /On Windows the same program prints \: MAIN_SEPARATOR is documented as '/' on Unix and '\\' on Windows. Windows parsing accepts both — a / is legal in a Path there too — but the one join writes is \. Because no block on this page hardcodes either character, all of it compiles and behaves correctly on both platforms from one source tree. That’s the whole trick: separators are the platform’s business, and PathBuf is where the platform gets its say.
Related
Section titled “Related”- How to find the path of the current executable — the config lookup that used
dir.join("config.toml") - How to read a file into a String — read the file at the path you just built
- Reading and writing files — the rest of the chapter