2611. [filesys.ts] [PDTS] Lack of relative() operation function

Section: 99 [filesys.ts::fs.filesystem.synopsis], 99 [filesys.ts::fs.op.funcs] Status: NAD Future Submitter: GB-1 Opened: 2014-01-20 Last modified: 2016-08-10

Priority: Not Prioritized

View all other issues in [filesys.ts::fs.filesystem.synopsis].

View all issues with NAD Future status.

Discussion:

Addresses: filesys.ts

There is no relative() operation, to complement both absolute() and canonical()

The TS introduces relative paths.

However there is no way to create a relative path as a path relative to another. Methods are provided to create absolute and canonical paths.

In section 15.1 Absolute [fs.op.absolute]:

path absolute(const path& p, const path& base=current_path());

and in section 15.2 Canonical [fs.op.canonical]

path canonical(const path& p, const path& base = current_path());

path canonical(const path& p, error_code& ec);

path canonical(const path& p, const path& base, error_code& ec);

By providing a operations to achieve absolute and canonical paths there is no impediment to providing a similar operation relative() that attempts to return a new path relative to some base path.

For example:

path relative(const path& p, const path& to = current_path());

path relative(const path& p, error_code& ec);

path relative(const path& p, const path& to, error_code& ec);

This would return a path, if possible, that is relative to to. The implementation can make use of absolute() and canonical() to determine the relative path, if it exists.

The File System TS is based on the boost::filesystem library and it too suffers from this anomaly. There are open tickets for this in Boost Trac:

and it is the subject of several posts on StackOverflow for example:

Other languages typically provide a similar function. For example python provides:

os.path.relpath(path[, start])

Return a relative filepath to path either from the current directory or from an optional start directory. This is a path computation: the filesystem is not accessed to confirm the existence or nature of path or start. start defaults to os.curdir.

[2014-02-07, Beman Dawes comments]

A relative() function is useful and much requested. I've seen such a function provided by users and have written it myself in app code. It is one of those things I've been meaning to do for years, and have just never gotten around to.

That said, my mild preference is to treat this as "NAD, Future" for File System TS1, but treat it as a priority for TS2.

[ 2014-02-11 Issaquah ]

The LWG/SG-3 voted strongly in favor of adding this functionality, and doing so in this TS. That implies quite a bit of work before the next meeting to validate that the proposed interface works as desired for various platforms. There was general agreement not to hold FS STS1 if this functionality isn't ready when the rest of the TS is ready.

[2014-05-19 Beman Dawes supplied wording.]

The design benefited from discussions with Jamie Allsop, who was the source of the original NB comment. Thanks to Bjorn Reese for corrections and suggestions. Although there was also discussion and experimentation with additional relative functions that took into account symlinks and normalization, these are not proposed here since even the proponents of such functions were unsure of appropriate semantics.

[2014-06-17 Rapperswil LWG closes as NAD, Future.]

Although there is strong concensus for eventually providing both lexical and existence based flavors of relative() functionality, discussion of the many possible design choices led to the conclusion that more research and actual user experience is necessary before moving forward. Interested parties should submit papers.

Original proposed resolution:

  1. Modify header <filesystem> synopsis, 6 [fs.filesystem.synopsis], by adding the operational functions after canonical:

    path relative(const path& p, const path& to = current_path());
    path relative(const path& p, error_code& ec);
    path relative(const path& p, const path& to, error_code& ec);
    
  2. Insert the section:

    15.3 Relative [fs.op.relative]

    path relative(const path& p, const path& to = current_path());
    path relative(const path& p, error_code& ec);
    path relative(const path& p, const path& to, error_code& ec);

    Overview: Return a relative path of p to the current directory or from an optional to path.

    Returns: A relative path such that canonical(to)/relative(p,to) == canonical(p), otherwise path(). If canonical(to) == canonical(p) the path path(".") is returned. For the overload without a to argument, to is current_path(). Signatures with argument ec return path() if an error occurs.

    Throws: As specified in Error reporting.

    Remarks: !exists(p) or !exists(to) or !is_directory(to) is an error.

    and bump all following sections up by 0.1. Update the contents and any cross-references accordingly.

Question: Should Returns be specified in terms of equivalence? For example: equivalent( canonical(to)/relative(p,to), canonical(p) )

Question: Should canonical(to) == canonical(p) return path(".") or path()? Why?

Question: Should to be spelled start?

Proposed resolution:

To 6 Header <experimental/filesystem> synopsis [fs.filesystem.synopsis], add:

path lexically_relative(const path& p, const path& base);

At the end of 8.6 path non-member functions [path.non-member], add

8.6.3 path lexically_relative function [path.lexically.relative]

path lexically_relative(const path& p, const path& base);

Creates a path from the trailing elements of p that are lexically relative to base, which must be a prefix of p.

Effects: If the number of elements in [ p.begin(), p.end() ) is less than or equal to the number of elements in [ base.begin(), base.end() ), or if any element in [base.begin(), base.end()) is not equal to the corresponding element in [p.begin(), p.end()), throw an exception of type filesystem_error.

Remarks: Equality or inequality are determined by path::operator== or path::operator!= respectively.

Returns: An object of class path containing the first element of p that does not have a corresponding element in base, followed by the subsequent elements of p appended as if by path::operator/=.

Throws: filesystem_error.

[Note: Behavior is determined by the lexical value of the elements of p and base - the external file system is not accessed. The case where an element of base is not equal to corresponding element of p is treated as an error to avoid returning an incorrect result in the event of symlinks.  --end note]

A possible implementation would be:

        
auto mm = std::mismatch( p.begin(), p.end(), base.begin(), base.end());
if (mm.first == p.end() || mm.second != base.end())
{
throw filesystem_error(
"p does not begin with base, so can not be made relative to base",
p, base,
error_code(errc::invalid_argument, generic_category()));
}
path tmp(*mm.first++);
for (; mm.first != p.end(); ++mm.first)
tmp /= *mm.first;
return tmp;