What is a macro for concatenating an arbitrary number of components to build a path in Rust?
Asked Answered
I

3

9

In Python, a function called os.path.join() allows concatenating multiple strings into one path using the path separator of the operating system. In Rust, there is only a function join() that appends a string or a path to an existing path. This problem can't be solved with a normal function as a normal function needs to have a fixed number of arguments.

I'm looking for a macro that takes an arbitrary number of strings and paths and returns the joined path.

Ingenuous answered 12/11, 2016 at 19:55 Comment(0)
R
5

Once you read past the macro syntax, it's not too bad. Basically, we take require at least two arguments, and the first one needs to be convertible to a PathBuf via Into. Each subsequent argument is pushed on the end, which accepts anything that can be turned into a reference to a Path.

macro_rules! build_from_paths {
    ($base:expr, $($segment:expr),+) => {{
        let mut base: ::std::path::PathBuf = $base.into();
        $(
            base.push($segment);
        )*
        base
    }}
}

fn main() {
    use std::{
        ffi::OsStr,
        path::{Path, PathBuf},
    };

    let a = build_from_paths!("a", "b", "c");
    println!("{:?}", a);

    let b = build_from_paths!(PathBuf::from("z"), OsStr::new("x"), Path::new("y"));
    println!("{:?}", b);
}
Raleigh answered 12/11, 2016 at 20:17 Comment(0)
C
9

There's a reasonably simple example in the documentation for PathBuf:

use std::path::PathBuf;
let path: PathBuf = [r"C:\", "windows", "system32.dll"].iter().collect();
Chagres answered 23/2, 2018 at 14:19 Comment(2)
This is nice. What is the r before "C:\"? I can see that without it the code doesn't compile, so I suppose it has to do with escaping the "\" character.Condescend
@PaulRazvanBerg It makes it a raw string literal. Which exactly as you said, makes it so you don't need to write two slashes "\\" in order to get a single slash.Chagres
R
5

Once you read past the macro syntax, it's not too bad. Basically, we take require at least two arguments, and the first one needs to be convertible to a PathBuf via Into. Each subsequent argument is pushed on the end, which accepts anything that can be turned into a reference to a Path.

macro_rules! build_from_paths {
    ($base:expr, $($segment:expr),+) => {{
        let mut base: ::std::path::PathBuf = $base.into();
        $(
            base.push($segment);
        )*
        base
    }}
}

fn main() {
    use std::{
        ffi::OsStr,
        path::{Path, PathBuf},
    };

    let a = build_from_paths!("a", "b", "c");
    println!("{:?}", a);

    let b = build_from_paths!(PathBuf::from("z"), OsStr::new("x"), Path::new("y"));
    println!("{:?}", b);
}
Raleigh answered 12/11, 2016 at 20:17 Comment(0)
D
3

A normal function which takes an iterable (e.g. a slice) can solve the problem in many contexts:

use std::path::{Path, PathBuf};

fn join_all<P, Ps>(parts: Ps) -> PathBuf
where
    Ps: IntoIterator<Item = P>,
    P: AsRef<Path>,
{
    parts.into_iter().fold(PathBuf::new(), |mut acc, p| {
        acc.push(p);
        acc
    })
}

fn main() {
    let parts = vec!["/usr", "bin", "man"];
    println!("{:?}", join_all(&parts));
    println!("{:?}", join_all(&["/etc", "passwd"]));
}

Playground

Donation answered 13/11, 2016 at 10:29 Comment(1)
The tradeoff is that you cannot have multiple types in the single call.Raleigh

© 2022 - 2024 — McMap. All rights reserved.