Extracting Path String(s) in Rust

A compilation-ready source for this article can be found here.

In Rust, it’s fairly straightforward to obtain the path of the running executable, any string passed to the executable, and so forth.

// crate declaration:
use std::env;

// somewhere in your code where env is in scope:

// store the executable arguments in a vector of String objects:
let args: Vec<String> = env::args().collect();

// create a String object named full_path and initialize it with the executable location:
let full_path: String = String::from(&args[0]);

// spit that string out:
println!("Executable path: {}", full_path);
Executable path: C:\Rust\rust_path_to_string\target\debug\path_to_string.exe

Great! Now we have a string and can do all sorts of operations on it. We’re done!

What do you mean you don’t want to do all sorts of string operations on it to extract the various parts? We’re amazing programmers, and our ability to write error-proof code is beyond reproach! However, another option exists. Allow me to introduce you to:

use std::path::Path;

let args: Vec<String> = env::args().collect();

let full_path = Path::new(&args[0]);

You can read more about it on this page. We are interested in these three functions:

  • pub fn parent(&self) -> Option<&Path>
  • pub fn file_stem(&self) -> Option<&OsStr>
  • pub fn extension(&self) -> Option<&OsStr>

Using them in our previous example to extract information about our executable might yield (note the missing ‘\’ and ‘.’ characters):

Executable parent: C:\Rust\rust_path_to_string\target\debug
Executable stem: path_to_string
Executable extension: exe

We must also note that these functions do not return Strings objects or &str (string slice). They return an Option type with either a reference to a Path object or an OsStr object. We can’t simply write:

// does not compile
println!("Executable parent: {}", full_path.parent());

We’re going to have to extract, if they exist, the references to Path and OsStr. The bad news is Path and OsStr, themselves cannot be be used directly as strings. The good news is that they both implement the to_string() method. The bad news is that the to_string() method on either can result in a failure. For each extraction, we’ll need to do two match challenges. The first way I’ve come up with looks like this:

let mut file_parent = String::new();

let _ = match full_path.parent() {
    Some(result) => {
        let _ = match result.to_str() {
            Some(value) => file_parent.clone_from(&value.to_string()),
            None => (),
        };
    },
    None => (),
};

Yes, that’s a mess. The outer match checks full_path.parent() to see if it’s Some(result) or None. If it’s Some(result) the inner match checks result.to_str() to see if it’s Some(value) or None. Only in the branch of Some(value) do we try to clone the str slice in value into file_parent via file_parent.clone_from(&value.to_string()). I’ve written this without caring about the return values, but a similar algorithm for the extension can also be written in this fashion noting that the declaration of file_ext can be put off until much later.

let some_value = match full_path.extension() {
    Some(value) => OsString::from(value),
    None => OsString::new(),
};

let file_ext = match some_value.to_str() {
    Some(content) => String::from(content),
    None => String::new(),
};

full_path.extension() matches Some(value) or None. If the match goes down the first arm, we create an OsString object from that value and pass it back up to some_value. If not, we create a new, blank OsString and assign it to some_value.

When returning from match arms, the objects MUST the same type. Initially, value was a borrowed reference to an OsString (&OsStr). We can’t safely create, at least in Rust, a borrowed reference to an object on the Heap and simply return its address.

some_value.to_str() matches Some(content) or None. Just as above, we create a new String from content or a blank String object via String::new().

At the end of the day, the default output on a Windows OS will look like:

Executable extension: exe

The first method did not care about the intermediate return types of parent() and to_str() only caring about the matching value in Some(value) and pass that pack to the String object file_parent. In that case the variables after let were simply named ‘_’ meaning we had no use for them. I think it’s a far more clever implementation, but it’s definitely more obfuscated.

Let’s say we have all three: file_parent, file_stem, and file_ext. Recall from before that ‘\’ and ‘.’ were missing. To recreate the full path name, we’d have to concatenate file_parent + ‘\\’ + file_stem + ‘.’ + file_ext to get the proper string.