How to find the path of the current executable
Short answer: std::env::current_exe() returns the full path of the running binary itself, as a PathBuf wrapped in io::Result. Join a filename onto exe.parent() and your config files are found next to the executable — whatever directory the program was launched from.
This is the other half of the working-directory problem from the previous page: current_dir() follows the shell that launched the program, so a config path built from it breaks the moment the binary is started from somewhere else. current_exe() asks a different question — where is the running binary itself — and the answer doesn’t move.
The path that doesn’t move
Section titled “The path that doesn’t move”From a fresh project — cargo new exe-demo, then replace the contents of src/main.rs with the block below and run cargo run:
use std::env;use std::path::PathBuf;
fn main() -> std::io::Result<()> { let exe: PathBuf = env::current_exe()?; let dir = exe.parent().expect("executable always has a parent"); let config = dir.join("config.toml"); println!("exe: {}", exe.display()); println!("config: {}", config.display()); Ok(())}exe: /Users/rango/scratch/exe-demo/target/debug/exe-democonfig: /Users/rango/scratch/exe-demo/target/debug/config.tomlThe paths are mine; yours will hang off your own project directory. The useful part is the shape: current_exe() hands back a PathBuf, .parent() trims the file name off it, and dir.join("config.toml") lands next to the binary. (expect is safe here — a running executable always has a containing directory.)
Now launch the same binary from somewhere else entirely:
$ cargo run -qexe: /Users/rango/scratch/exe-demo/target/debug/exe-democonfig: /Users/rango/scratch/exe-demo/target/debug/config.toml$ cd /Users/rango && /Users/rango/scratch/exe-demo/target/debug/exe-demoexe: /Users/rango/scratch/exe-demo/target/debug/exe-democonfig: /Users/rango/scratch/exe-demo/target/debug/config.tomlSame answer from both. The working directory is process state copied in at launch, so it changes when you cd; the executable’s path comes from the OS’s record of what was started, and it doesn’t care where you stood. Build your config path from current_exe() instead of current_dir() and the program finds its files no matter how it was launched.
If it was launched through a symlink
Section titled “If it was launched through a symlink”Putting a symlink to a binary on $PATH instead of copying it is common practice. Watch what current_exe() reports for a launch through one (bash on macOS, Rust 1.99.0):
$ ln -s /Users/rango/scratch/exe-demo/target/debug/exe-demo /Users/rango/scratch/exe-link$ /Users/rango/scratch/exe-linkexe: /Users/rango/scratch/exe-linkconfig: /Users/rango/scratch/config.tomlThe program is the same file, but the reported path is the link’s, and the config lookup has moved with it — config.toml would now be read from /Users/rango/scratch, next to the link, not next to the real binary. The std doc is explicit that this is platform-dependent: some platforms return the path of the symbolic link, others the path of its target. On macOS you get the link. If that distinction matters, resolve it:
use std::env;use std::fs;use std::path::PathBuf;
fn main() -> std::io::Result<()> { let exe: PathBuf = env::current_exe()?; let real = fs::canonicalize(&exe)?; println!("current_exe: {}", exe.display()); println!("canonicalize: {}", real.display()); Ok(())}Run through the same link:
current_exe: /Users/rango/scratch/exe-linkcanonicalize: /Users/rango/scratch/exe-demo/target/debug/exe-demofs::canonicalize follows every symlink on the path and returns the real location, so the config lookup lands next to the actual binary no matter how the program was reached. It returns io::Result too, for the usual reason: the path it’s given might not resolve.
Trust it this far
Section titled “Trust it this far”Two caveats come straight from the std doc. If the binary is renamed while it is running, the platform may report the path it was loaded under rather than the new name — irrelevant for most programs, worth knowing if you write a self-updating one.
The bigger one is security: on some Unix platforms the result is worked out by searching $PATH for argv[0], and both can be set to arbitrary text by whoever launches the program. A setuid or setgid tool shouldn’t trust this path for anything security-relevant. For finding your own config file next to your own binary, it’s the right answer.
Like current_dir(), the return type is io::Result — acquiring the path is a platform-specific operation that can fail. In practice it rarely does, but the ? costs nothing and keeps main honest.
Related
Section titled “Related”- How to get the current working directory — the other “where am I?” question, and why the two answers differ
- How to read a file into a String —
fs::read_to_stringon the config path you just built - Reading and writing files — the rest of the chapter