If a user wants to create a path using a builder pattern (path.join(a).join(b).join(c)), they are inadvertently copying the string buffer on every call to join. This means to easiest way to write the code is the least efficient. The efficient version takes effort and requires the developer to either read the source code or look at the docs to know that join allocates memory (so they have to use push instead):
{
let tmp = path.join(a);
tmp.push(b);
tmp.push(c);
tmp
}
Underlying issue
join actually comes from Path and allocates a new PathBuf. Calling PathBuf::join will deref coerce down to a Path and then come back with a newly allocated PathBuf.
Potential solutions
New methods
Add new methods to PathBuf such as PathBuf::append, PathBuf::using_extension, and PathBuf::using_file_name.
I don't like this because it still means you have to understand the difference between join/append and why one might be better than the other.
Break people's code (maybe in the next edition?)
Add new methods to PathBuf called PathBuf::join, PathBuf::with_extension, and PathBuf::with_file_name. This will break people's code if they depend on PathBuf::join creating a new owned instance. Going back to old behavior is very easy: (*path).join instead of path.join.
Is a stdlib change like this possible? How would it work?
Finally, a practical reason to implement Div for PathBuf, so we can join paths with a / b / c.
(Only slightly less-controversially, we could implement Add for PathBuf and write this as a + b + c, as we already do for String.)
Note that in many cases the efficiency benefit of repeated push over join is small or even zero, since each push might also trigger a reallocation and copy. I expect the difference is rarely important in practice except when the number of join calls is fairly high.
I believe it's possible to write e.g. path.extend([a, b, c]) if you have PathBuf to push multiple path fragments in one go, which is another option to use.
Or you can implement both Div and Add with former using push and latter just appending string (BTW, I do not see how to do it conveniently, and I just happened to have to do exactly that (specifically, appending extension to a file name possibly with an extension) a week ago).
Is that possible? Are there docs I can look at to understand how conditionally adding code by edition would work?
I think that's the best option given that the array syntax requires each path segment to be of the same type. And while the tap library looks pretty sweet (thanks for sharing!), it requires knowing how the path APIs are implemented to seek something like that out.
I would not say that just push is what would always be expected result for path / ..: /foo/bar/.. may very well be resolved to /baz because /foo/bar is actually a symlink to /baz/bar. Probably better to leave this out.
I believe {Path, PathBuf} don't currently try to resolve either, favoring the verbatim path indicated by the code and leaving the resolution to other code.
If I'm correct in that assertion, then I don't see the issue with the path! macro as defined above.
Path and PathBuf simply treat paths as segments, without resolution as every push (or change in general) would require to check the filesystem for symlinks etc.
Normalization is performed with std::fs::canonicalize for that matter.
Path and PathBuf treat paths as an opaque blob of bytes until you call a specific function (which should document its behaviour). Most of these functions are in some way based on iterating components but things like push essentially just glue bytes together. It's why \0 in paths is valid. Or why you can push. filenames or have trailing slashes even though components ignores these.
Filesystem functions (some of which are implemented on Path) can return a new PathBuf but that is just whatever the OS returns after we call the relevant function.
Personally I feel there's an opportunity with path! macro to do some normalization or to have a compile error on "weird" paths. Though making it suitable for cross-platform code might be an interesting problem.