Trailing '/' in paths

How would you efficiently detect a trailing '/' in a PathBuf or in a AsRef<Path> ?(i.e. the difference between PathBuf::from("/home") and PathBuf::from("/home/") ?

And how would you add a trailing '/' to a PathBuf not having one ?

In both cases I see no other way than using to_str.

Unfortunately I don't think there's a good way. using PathBuf or Path. This is (hopefully) going to be improved in the future but for now here are some ideas I came up with:

#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;

use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};

#[cfg(windows)]
fn has_trailing_slash(p: &Path) -> bool {
    let last = p.as_os_str().encode_wide().last();
    last == Some(b'\\' as u16) || last == Some(b'/' as u16)
}
#[cfg(unix)]
fn has_trailing_slash(p: &Path) -> bool {
    p.as_os_str().as_bytes().last() == Some(&b'/')
}

fn add_trailing_slash(p: &mut PathBuf) {
    let fname = p.file_name();
    let dirname = if let Some(fname) = fname {
        let mut s = OsString::with_capacity(fname.len() + 1);
        s.push(fname);
        if cfg!(windows) {
            s.push("\\");
        } else {
            s.push("/");
        }
        s
    } else {
        OsString::new()
    };
    
    if p.pop() {
        p.push(dirname);
    }
}

Playground link

Maybe this can be improved upon or there's a better way I don't know of.

1 Like

While there probably is some unsafe and incorrect way to peek at the last byte of the contained OsStr, that solution not going to be portable anyway. I suspect your challenge is bigger than just I get PathBufs and have to efficiently add a slash if there isn't already one. Note internally that might not even be a slash, can be any valid path separator on that platform, or some other way of encoding it. If you share a bigger picture, I'd be glad to give suggestions.

Why bother? They are equal other than displayed.

Without knowing OS.

    let mut pb = PathBuf::from("/home");
    if !pb.as_os_str().is_empty() {
        let mut name = pb.file_name().map(ToOwned::to_owned).unwrap_or_default();
        name.push(std::path::MAIN_SEPARATOR.to_string());
        pb.set_file_name(name);
    }

    let s = pb.as_os_str().to_string_lossy();
    assert!(s.ends_with(&std::path::MAIN_SEPARATOR.to_string()));

Path and PathBuf have ends_with() method. PathBuf has push() which can alter the object

They are different by practice. For example in Linux, if the path points to a symlink targetting another directory, the / at the end signals to follow the symlink.

3 Likes

That doesn't work here Rust Playground

1 Like

Yes, ends_with can't work because /, when not at the end, isn't a Component.

As for why make the difference, there are several reasons. My precise use case is related to auto-completion (a/b can be completed in a/ba and a/b/ while a/b/ should be completed with the childs of b). It's really possible to only deal with strings, that's what I do, but in a complex application this means keeping a lot of strings just for that while paths are supposed to wrap them and shouldn't lose information.

Ah right, Path::ends_with() is defined in terms of Path::components() which does some kinds of normalization.

But you can still use push("") for addressing your original problem, actually regardless whether your PathBuf has it already or not

3 Likes

That does indeed work. Using push with an empty string is not at all obvious or documented. But the behaviour does make some sense once you realise it's implemented in terms of components and not strings.

So, pushing "" seems to be the simplest solution to add a trailing /.

Unfortunately, any path seems to return true on pb.ends_with("")...

Shouldn't that last point be considered a bug (if fixed, a "" constant could be defined to make the meaning of this empty component clearer)?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.