Search code examples
rustpathiteratortype-conversion

Converting a generic Iterator (e.g. SkipWhile) back to a specialised iterator (e.g. std::path::Components)


To avoid the XY problem, here's what I originally want to do: I have a path: /foo/bar/bloo/baz/file.txt and would like the most efficient way to obtain bloo/baz/file.txt from that, essentially skipping all ancestors before a specific component (in this case "bloo"). The approach that seemed most sensible to me was to use the components function in std::path::Path.

My attempt looks like this:

let new_path = path
    .components()
    .skip_while(|v| if let Normal(v) = v { v.as_bytes() != b"bloo" } else { true })
    .skip(1)
    .as_path();

However this produces the following error:

error[E0599]: no method named `as_path` found for struct `Skip` in the current scope
   --> src/main.rs:712:14
    |
712 |             .as_path();
    |              ^^^^^^^ method not found in `Skip<SkipWhile<Components<'_>, [closure@src/main.rs:710:25: 710:89]>>`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `bloggers` due to previous error

Which is pretty self explanatory. I could just collect the remaining components into a string using the underlying filesystem separator, but that seems like more of a workaround than the actual solution.

In a more general sense: Can we convert a generic iterator such as Skip or Map back to a specialised iterator such as Components, DirEntry, etc. as long as we can guarantee that the Item type is the same?

edit#1: For reference, the most efficient solution I could think of to build a new path from a string looks as follows (using the unstable intersperse and MAIN_SEPARATOR_STR features):

let new_path: String = path
    .components()
    .skip_while(|v| if let Normal(v) = v { v.as_bytes() != b"bloo" } else { true })
    .skip(1)
    .map(|v| v.as_os_str().to_str())
    .intersperse(Some(std::path::MAIN_SEPARATOR_STR))
    .collect::<Option<_>>()
    .ok_or_else(|| /* error reporting of choice */)?;

Solution

  • The simplest way is to consume the iterator eagerly:

    let mut new_path = path.components();
    while let Some(v) = new_path.next() {
        if let Normal(v) = v {
            if v.as_bytes() == b"bloo" { // Can be just `v == "bloo"`
                break;
            }
        }
    }
    let new_path = new_path.as_path();
    

    itertools has the dropping() method for an eager skip(), but doesn't have an eager equivalent to skip_while(), unfortunately, so you have to roll your own, like I did.