Get relative path from two absolute paths

24,287

Solution 1

As of version 1.60.0 boost.filesystem does support this. You're looking for the member function path lexically_relative(const path& p) const.

Original, pre-1.60.0 answer below.


Boost doesn't support this; it's an open issue — #1976 (Inverse function for complete) — that nevertheless doesn't seem to be getting much traction.

Here's a vaguely naive workaround that seems to do the trick (not sure whether it can be improved):

#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/fstream.hpp>
#include <stdexcept>

/**
 * https://svn.boost.org/trac/boost/ticket/1976#comment:2
 * 
 * "The idea: uncomplete(/foo/new, /foo/bar) => ../new
 *  The use case for this is any time you get a full path (from an open dialog, perhaps)
 *  and want to store a relative path so that the group of files can be moved to a different
 *  directory without breaking the paths. An IDE would be a simple example, so that the
 *  project file could be safely checked out of subversion."
 * 
 * ALGORITHM:
 *  iterate path and base
 * compare all elements so far of path and base
 * whilst they are the same, no write to output
 * when they change, or one runs out:
 *   write to output, ../ times the number of remaining elements in base
 *   write to output, the remaining elements in path
 */
boost::filesystem::path
naive_uncomplete(boost::filesystem::path const p, boost::filesystem::path const base) {

    using boost::filesystem::path;
    using boost::filesystem::dot;
    using boost::filesystem::slash;

    if (p == base)
        return "./";
        /*!! this breaks stuff if path is a filename rather than a directory,
             which it most likely is... but then base shouldn't be a filename so... */

    boost::filesystem::path from_path, from_base, output;

    boost::filesystem::path::iterator path_it = p.begin(),    path_end = p.end();
    boost::filesystem::path::iterator base_it = base.begin(), base_end = base.end();

    // check for emptiness
    if ((path_it == path_end) || (base_it == base_end))
        throw std::runtime_error("path or base was empty; couldn't generate relative path");

#ifdef WIN32
    // drive letters are different; don't generate a relative path
    if (*path_it != *base_it)
        return p;

    // now advance past drive letters; relative paths should only go up
    // to the root of the drive and not past it
    ++path_it, ++base_it;
#endif

    // Cache system-dependent dot, double-dot and slash strings
    const std::string _dot  = std::string(1, dot<path>::value);
    const std::string _dots = std::string(2, dot<path>::value);
    const std::string _sep = std::string(1, slash<path>::value);

    // iterate over path and base
    while (true) {

        // compare all elements so far of path and base to find greatest common root;
        // when elements of path and base differ, or run out:
        if ((path_it == path_end) || (base_it == base_end) || (*path_it != *base_it)) {

            // write to output, ../ times the number of remaining elements in base;
            // this is how far we've had to come down the tree from base to get to the common root
            for (; base_it != base_end; ++base_it) {
                if (*base_it == _dot)
                    continue;
                else if (*base_it == _sep)
                    continue;

                output /= "../";
            }

            // write to output, the remaining elements in path;
            // this is the path relative from the common root
            boost::filesystem::path::iterator path_it_start = path_it;
            for (; path_it != path_end; ++path_it) {

                if (path_it != path_it_start)
                    output /= "/";

                if (*path_it == _dot)
                    continue;
                if (*path_it == _sep)
                    continue;

                output /= *path_it;
            }

            break;
        }

        // add directory level to both paths and continue iteration
        from_path /= path(*path_it);
        from_base /= path(*base_it);

        ++path_it, ++base_it;
    }

    return output;
}

Solution 2

With C++17 and its std::filesystem::relative, which evolved from boost, this is a no-brainer:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main()
{
    const fs::path base("/is/the/speed/of/light/absolute");
    const fs::path p("/is/the/speed/of/light/absolute/or/is/it/relative/to/the/observer");
    const fs::path p2("/little/light/races/in/orbit/of/a/rogue/planet");
    std::cout << "Base is base: " << fs::relative(p, base).generic_string() << '\n'
              << "Base is deeper: " << fs::relative(base, p).generic_string() << '\n'
              << "Base is orthogonal: " << fs::relative(p2, base).generic_string();
    // Omitting exception handling/error code usage for simplicity.
}

Output (second parameter is base)

Base is base: or/is/it/relative/to/the/observer
Base is deeper: ../../../../../../..
Base is orthogonal: ../../../../../../little/light/races/in/orbit/of/a/rogue/planet

It uses std::filesystem::path::lexically_relative for comparison. The difference to the pure lexical function is, that std::filesystem::relative resolves symlinks and normalizes both paths using std::filesystem::weakly_canonical (which was introduced for relative) before comparison.

Solution 3

I just wrote code that can translate an absolute path to a relative path. It works in all my use cases, but I can not guarantee it is flawless.

I have abreviated boost::filesystem to 'fs' for readability. In the function definition, you can use fs::path::current_path() as a default value for 'relative_to'.

fs::path relativePath( const fs::path &path, const fs::path &relative_to )
{
    // create absolute paths
    fs::path p = fs::absolute(path);
    fs::path r = fs::absolute(relative_to);

    // if root paths are different, return absolute path
    if( p.root_path() != r.root_path() )
        return p;

    // initialize relative path
    fs::path result;

    // find out where the two paths diverge
    fs::path::const_iterator itr_path = p.begin();
    fs::path::const_iterator itr_relative_to = r.begin();
    while( itr_path != p.end() && itr_relative_to != r.end() && *itr_path == *itr_relative_to ) {
        ++itr_path;
        ++itr_relative_to;
    }

    // add "../" for each remaining token in relative_to
    if( itr_relative_to != r.end() ) {
        ++itr_relative_to;
        while( itr_relative_to != r.end() ) {
            result /= "..";
            ++itr_relative_to;
        }
    }

    // add remaining path
    while( itr_path != p.end() ) {
        result /= *itr_path;
        ++itr_path;
    }

    return result;
}

Solution 4

I was just thinking about using boost::filesystem for the same task, but - since my application uses both Qt and Boost libraries, I decided to use Qt which does this task with one simple method QString QDir::relativeFilePath( const QString & fileName ):

QDir dir("/home/bob");
QString s;

s = dir.relativeFilePath("images/file.jpg");     // s is "images/file.jpg"
s = dir.relativeFilePath("/home/mary/file.txt"); // s is "../mary/file.txt"

It works like a charm and saved me a few hours of my life.

Solution 5

Here's how I do it in the library I build on top of boost filesystem:

Step 1: Determine "deepest common root". Basically, its like the greatest common denominator for 2 paths. For example, if you're 2 paths are "C:\a\b\c\d" and "C:\a\b\c\l.txt" then the common root they both share is "C:\a\b\c\".

To get this, convert both paths into absolute- NOT canonical- form (you'll want to be able to do this for speculative paths & symlinks).

Step 2: To go from A to B, you suffix A with enough copies of "../" to shift up the directory tree to the common root, then add the string for B to travel down the tree to it. On windows you can have 2 paths with no common root, so going from any A to any B is not always possible.

namespace fs = boost::filesystem;

bool GetCommonRoot(const fs::path& path1,
                       const fs::path& path2,
                       fs::path& routeFrom1To2,
                       std::vector<fs::path>& commonDirsInOrder)
{
   fs::path pathA( fs::absolute( path1));
   fs::path pathB( fs::absolute( path2));

   // Parse both paths into vectors of tokens. I call them "dir" because they'll
   // be the common directories unless both paths are the exact same file.
   // I also Remove the "." and ".." paths as part of the loops

   fs::path::iterator    iter;
   std::vector<fs::path> dirsA;
   std::vector<fs::path> dirsB;
   for(iter = pathA.begin(); iter != pathA.end(); ++iter) {
       std::string token = (*iter).string();
       if(token.compare("..") == 0) {      // Go up 1 level => Pop vector
          dirsA.pop_back();
       }
       else if(token.compare(".") != 0) {  // "." means "this dir" => ignore it
          dirsA.push_back( *iter);
       }
   }
   for(iter = pathB.begin(); iter != pathB.end(); ++iter) {
       std::string token = (*iter).string();
       if(token.compare("..") == 0) {      // Go up 1 level => Pop vector
          dirsB.pop_back();
       }
       else if(token.compare(".") != 0) {  // "." means "this dir" => ignore it
          dirsB.push_back( *iter);
       }
   }

   // Determine how far to check in each directory set
   size_t commonDepth = std::min<int>( dirsA.size(), dirsB.size());
   if(!commonDepth) {
       // They don't even share a common root- no way from A to B
       return false;
   }

   // Match entries in the 2 vectors until we see a divergence
   commonDirsInOrder.clear();
   for(size_t i=0; i<commonDepth; ++i) {
      if(dirsA[i].string().compare( dirsB[i].string()) != 0) {   // Diverged
         break;
      }
      commonDirsInOrder.push_back( dirsA[i]);  // I could use dirsB too.
   }

   // Now determine route: start with A
   routeFrom1To2.clear();
   for(size_t i=0; i<commonDepth; ++i) {
       routeFrom1To2 /= dirsA[i];
   }
   size_t backupSteps = dirsA.size() - commonDepth; // # of "up dir" moves we need
   for(size_t i=0; i<backupSteps; ++i) {
       routeFrom1To2 /= "../";
   }

   // Append B's path to go down to it from the common root
   for(size_t i=commonDepth; i<dirsB.size(); ++i) {
       routeFrom1To2 /= dirsB[i];    // ensures absolutely correct subdirs
   }
   return true;

}

This will do what you want- you go up from A until you hit the common folder it and B are both descendants of, then go down to B. You probably don't need the "commonDirsInOrder" return that I have, but the "routeFrom1To2" return IS the one you're asking for.

If you plan to actually change the working directory to "B" you can use "routeFrom1To2" directly. Be aware that this function will produce an absolute path despite all the ".." parts, but that shouldn't be a problem.

Share:
24,287
Lightness Races in Orbit
Author by

Lightness Races in Orbit

Retired. Pet hates: Answers in the comments section "Ternary operator" Comma splices

Updated on May 01, 2020

Comments

  • Lightness Races in Orbit
    Lightness Races in Orbit almost 4 years

    I have two absolute filesystem paths (A and B), and I want to generate a third filesystem path that represents "A relative from B".

    Use case:

    • Media player managing a playlist.
    • User adds file to playlist.
    • New file path added to playlist relative to playlist path.
    • In the future, entire music directory (including playlist) moved elsewhere.
    • All paths still valid because they are relative to the playlist.

    boost::filesystem appears to have complete to resolve relative ~ relative => absolute, but nothing to do this in reverse (absolute ~ absolute => relative).

    I want to do it with Boost paths.

  • sehe
    sehe about 13 years
    Thanks for sharing that. I too spent some length of time searching in wonder of this missing feature
  • EPonyA
    EPonyA almost 13 years
    This code does not work with version 3 of boost::filesystem due to the use of dot and slash, but with some modifications it can work.
  • Lightness Races in Orbit
    Lightness Races in Orbit almost 13 years
    @Phineas: Modified code posted as an answer would be appreciated :)
  • EPonyA
    EPonyA almost 13 years
    I didn't have working code when I wrote the comment. Comment out the using lines with dot and slash, then set _dot, _dots, and _sep to ".", "..", and "/" respectively. I'm not sure what happened to dot and slash in V3 or whether there is a replacement.
  • Lightness Races in Orbit
    Lightness Races in Orbit almost 13 years
    @Phineas: I think that would be a backwards step. I left them deliberately abstracted (albeit, admittedly, quite poorly). If dot and slash have been removed in v3 then I'd like to understand why, and I'd like to find out what abstract mechanism should replace them.
  • metal
    metal about 11 years
    Naivete #1: it is case sensitive, which it shouldn't be, on Windows systems at least. Naivete #2: It does not handle symlinks correctly.
  • Rawler
    Rawler about 11 years
    Hmm. I think the "if( itr_relative_to != r.end() )"-part may be causing, at least to me, unexpected behavior. Say for example I want to have a relative path, to /home/adam/file from my home-directory, /home/ulrik. This implementation will for relativePath("/home/adam/file", "/home/ulrik") return "adam/file". It seems the implementation expects a trailing "/", but for example path("/home/ulrik/file").parent_path() has no such trailing slash. I believe the "if (itr_relative_to)" should be dropped.
  • Mad Physicist
    Mad Physicist over 9 years
    /n? Someone got trigger-happy with the replace button. (Fixed)
  • Mad Physicist
    Mad Physicist over 9 years
    Good solution. Symlinks can be handled by making everything canonical before beginning. Otherwise you can't know that something is a symlink to begin with.
  • rr-
    rr- over 8 years
    Following the posted link reveals that this is getting accepted into the standard.
  • Lightness Races in Orbit
    Lightness Races in Orbit almost 6 years
    Loving those examples :D Glad the standardised version got, if you'll pardon the pun, completed
  • applesoup
    applesoup almost 5 years
    For me, this relativePath() exits in certain situations because one of the {itr_path, itr_relative_to} iterators may already be equal to their respective .end(). The comparison *itr_path==*itr_relative_to is not possible in this case. Moving the comparison after the ... != .end() checks solves this problem.
  • Paul Houx
    Paul Houx almost 5 years
    Thanks, @applesoup, I've changed the code to reflect your remark.