Skip to content
Select theme

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.

From a fresh project — cargo new exe-demo, then replace the contents of src/main.rs with the block below and run cargo run:

exe: /Users/rango/scratch/exe-demo/target/debug/exe-demo
config: /Users/rango/scratch/exe-demo/target/debug/config.toml

The 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:

Terminal window
$ cargo run -q
exe: /Users/rango/scratch/exe-demo/target/debug/exe-demo
config: /Users/rango/scratch/exe-demo/target/debug/config.toml
$ cd /Users/rango && /Users/rango/scratch/exe-demo/target/debug/exe-demo
exe: /Users/rango/scratch/exe-demo/target/debug/exe-demo
config: /Users/rango/scratch/exe-demo/target/debug/config.toml

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

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):

Terminal window
$ ln -s /Users/rango/scratch/exe-demo/target/debug/exe-demo /Users/rango/scratch/exe-link
$ /Users/rango/scratch/exe-link
exe: /Users/rango/scratch/exe-link
config: /Users/rango/scratch/config.toml

The 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:

Run through the same link:

current_exe: /Users/rango/scratch/exe-link
canonicalize: /Users/rango/scratch/exe-demo/target/debug/exe-demo

fs::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.

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.