Better way to get an absolute file path?

Hey everyone,
I am looking for a good best-effort conversion of a relative to absolute file path.

So far I came up with an abomination using the shellexpand crate and Path::canonicalize. Plus it doesn't look like an idiomatic Rust to me.

fn abspath(p: &str) -> Option<String> {
    shellexpand::full(p)
        .ok()
        .and_then(|x| Path::new(&x.into_owned()).canonicalize().ok())
        .and_then(|x| x.into_os_string().into_string().ok())
}

Can this code be improved? I am not worried about symlinks and hardlinks for the moment.

Do you only need to convert relative path to the absolute one? If so you don't need shellexpand and links are dealt with by std::fs::canonicalize():

Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved.

1 Like

I am using shellexpand simply because I want the tilde (~) handled as well.

I tested my code with three paths:

  • ~/some.db
  • /Users/dimi/some.db
  • ../../some.db (the project is two levels below my homedir)

And it works and I can accept your feedback on functionality as the final stance -- but the code just looks so very ugly. I am still learning the borrow checker and it seemed to me I am fighting with it and not working with it. Am I wrong? Is the code okay then? Is there a way to do it with less chaining?

You could:

  • change that Path::new(&x.into_owned()) to Path::new(x.as_ref()) to avoid a potential allocation
  • change what now is Path::new(x.as_ref()).canonicalize() to std::fs::canonicalize(x.as_ref()) to avoid importing Path and shortening the code a bit more
  • use the ? operator to avoid calling and_then over and over
fn abspath(p: &str) -> Option<String> {
    let exp_path = shellexpand::full(p).ok()?;
    let can_path = std::fs::canonicalize(exp_path.as_ref()).ok()?;
    can_path.into_os_string().into_string().ok()
}

I feel like this could be better if:

  • there was an implementation of AsRef<Path> for Cow<'_ str> to avoid that .as_ref()
  • there was a way to directly convert a PathBuf to a String, without passing through an OsString

However I don't think this is so bad overall

1 Like

Chaining is fine. Here are two more idiomatic versions, though:

  • abspath() has one fewer allocation (uses as_ref() instead of into_owned());
  • abspath_buf() skips the conversion to String altogether, so that it doesn't fail unnecessarily if the result is not valid UTF-8.
fn abspath(p: &str) -> Option<String> {
    shellexpand::full(p)
        .ok()
        .and_then(|x| Path::new(OsStr::new(x.as_ref())).canonicalize().ok())
        .and_then(|p| p.into_os_string().into_string().ok())
}

fn abspath_buf(p: &str) -> Option<PathBuf> {
    shellexpand::full(p)
        .ok()
        .and_then(|x| Path::new(OsStr::new(x.as_ref())).canonicalize().ok())
}
2 Likes