Skip to content
Select theme

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.

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:

by hand: logs//app.txt
join: logs/app.txt

Path 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.

push: logs/app.txt
join abs: /etc/hosts

Two 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.

None of the code above types a separator character. On this machine (macOS, Rust 1.99.0) the one std uses is:

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.