从两个绝对路径获取相对路径

34

我有两个绝对文件系统路径(A和B),我想生成第三个文件系统路径,表示“A相对于B的路径”。

使用情况:

  • 媒体播放器管理播放列表。
  • 用户将文件添加到播放列表。
  • 新文件路径相对于播放列表路径添加到播放列表。
  • 未来,整个音乐目录(包括播放列表)可能会移动到其他地方。
  • 所有路径依然有效,因为它们是相对于播放列表的。

boost::filesystem似乎有complete用于解析relative ~ relative => absolute,但没有做相反操作(absolute ~ absolute => relative)的函数。

我想使用Boost路径来完成这个操作。


与 https://dev59.com/GlbTa4cB1Zd3GeqP9U6m 密切相关(但在我看来更全面)。 - Lightness Races in Orbit
还有几个相关的问题(但不完全相同):https://dev59.com/y3VC5IYBdhLWcg3wfxM8,和https://dev59.com/iHE85IYBdhLWcg3w43sW。它们分别用.NET和shell来表述,但都在寻求相同的基本功能。这些答案可能会有所帮助... - Jonathan Leffler
可能是如何从绝对路径获取相对路径的重复问题。 - user9645477
1
@VictorKwan:那个问题涉及到完全不同的技术... - Lightness Races in Orbit
7个回答

23

使用C++17及其从boost演化而来的std::filesystem::relative,这是易如反掌的:

#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.
}

输出(第二个参数是底数)

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
它使用std::filesystem :: path :: lexically_relative 进行比较。 与纯字典排序函数的区别在于,std::filesystem::relative解析符号链接并使用std::filesystem::weakly_canonical(为relative引入)规范化两个路径后再进行比较。

1
喜欢那些例子:D 很高兴标准化版本得到了完成,如果你原谅双关语的话。 - Lightness Races in Orbit

19

从版本 1.60.0 开始,boost.filesystem 支持此功能。您需要查找成员函数 path lexically_relative(const path& p) const

以下是 1.60.0 版本之前的原始答案。


Boost不支持此功能,这是一个未解决的问题#1976(完整反函数),但似乎没有得到很多关注。
以下是一个比较天真的解决方法,似乎可以解决问题(不确定是否可以改进):
#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;
}

1
谢谢分享。我也花了一些时间想要找到这个缺失的功能。 - sehe
这段代码由于使用了点和斜杠,在boost::filesystem的第3版中无法工作,但是通过一些修改可以使其工作。 - May Oakes
1
@Phineas:希望你能提供修改后的代码作为答案 :) - Lightness Races in Orbit
天真错误#1:它是大小写敏感的,至少在Windows系统上不应该如此。天真错误#2:它无法正确处理符号链接。 - metal
点击发布的链接可以发现,这已经被纳入标准中了。 - rr-
显示剩余3条评论

8

我刚刚编写了能够将绝对路径转换为相对路径的代码。在我的所有使用情况中都有效,但我不能保证它是完美无缺的。

为了可读性,我将boost::filesystem缩写为'fs'。在函数定义中,您可以将fs::path::current_path()用作'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;
}

嗯。我认为“if(itr_relative_to!= r.end())”部分可能会导致意外行为,至少对我来说是这样。例如,假设我想从我的主目录/home/ulrik到/home/adam/file的相对路径。此实现将返回"adam/file",但它似乎期望有一个尾随"/",但是例如path("/home/ulrik/file").parent_path()没有这样的尾随斜杠。我认为应该删除“if (itr_relative_to)”。 - Rawler
对我来说,relativePath() 在某些情况下存在,因为其中一个 {itr_path, itr_relative_to} 迭代器可能已经等于它们各自的 .end()。在这种情况下,比较 *itr_path==*itr_relative_to 是不可能的。将比较移动到 ... != .end() 检查之后可以解决这个问题。 - applesoup
1
谢谢@applesoup,我已经根据你的评论修改了代码。 - Paul Houx

6

我在考虑使用boost::filesystem来完成相同的任务,但是由于我的应用程序同时使用了Qt和Boost库,我决定使用Qt来处理这个任务,因为它只需要使用一个简单的方法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"

它非常好用,让我省下了几个小时的时间。


2

我已经为这个技巧写下了一个简单的解决方案。

没有使用boost库,只用到STL的std::stringstd::vector

在Win32平台上进行了测试。

只需调用:

strAlgExeFile = helper.GetRelativePath(PathA, PathB);

它会返回从PathAPathB的相对路径。

例如:

strAlgExeFile = helper.GetRelativePath((helper.GetCurrentDir()).c_str(), strAlgExeFile.c_str());

#ifdef _WIN32                                                                              
    #define STR_TOKEN "\\"                                                                 
    #define LAST_FOLDER "..\\"                                                             
    #define FOLDER_SEP "\\"                                                                
    #define LINE_BREAK "\r\n"                                                              
#else                                                                                      
    #define STR_TOKEN "/"                                                                  
    #define LAST_FOLDER "../"                                                              
    #define FOLDER_SEP "/"                                                                 
    #define LINE_BREAK "\n"                                                                
#endif // _WIN32                                                                           

void CHelper::SplitStr2Vec(const char* pszPath, vector<string>& vecString)                 
{                                                                                          
  char * pch;                                                                              

  pch = strtok (const_cast < char*> (pszPath), STR_TOKEN );                                
  while (pch != NULL)                                                                      
  {                                                                                        
    vecString.push_back( pch );                                                            
    pch = strtok (NULL, STR_TOKEN );                                                       
  }                                                                                        
}                                                                                          

string& CHelper::GetRelativePath(const char* pszPath1,const char* pszPath2)                
{                                                                                          
    vector<string> vecPath1, vecPath2;                                                     
    vecPath1.clear();                                                                      
    vecPath2.clear();                                                                      
    SplitStr2Vec(pszPath1, vecPath1);                                                      
    SplitStr2Vec(pszPath2, vecPath2);                                                      
    size_t iSize = ( vecPath1.size() < vecPath2.size() )? vecPath1.size(): vecPath2.size();
    unsigned int iSameSize(0);                                                             
    for (unsigned int i=0; i<iSize; ++i)                                                   
    {                                                                                      
        if ( vecPath1[i] != vecPath2[i])                                                   
        {                                                                                  
            iSameSize = i;                                                                 
            break;                                                                         
        }                                                                                  
    }                                                                                      

    m_strRelativePath = "";                                                                
    for (unsigned int i=0 ; i< (vecPath1.size()-iSameSize) ; ++i)                          
        m_strRelativePath += const_cast<char *> (LAST_FOLDER);                             

    for (unsigned int i=iSameSize ; i<vecPath2.size() ; ++i)                               
    {                                                                                      
        m_strRelativePath += vecPath2[i];                                                  
        if( i < (vecPath2.size()-1) )                                                      
            m_strRelativePath += const_cast<char *> (FOLDER_SEP);                          
    }                                                                                      

    return m_strRelativePath;                                                              
}

有人在替换按钮上手太重了。(已修复) - Mad Physicist

2

以下是我在基于boost文件系统构建的库中的操作步骤:

步骤1:确定“最深公共根”。基本上,它类似于2个路径的最大公约数。例如,如果你有2个路径:“C:\a\b\c\d”和“C:\a\b\c\l.txt”,那么它们共享的公共根是“C:\a\b\c\”。

为了获得这个公共根,将两个路径转换为绝对而不是规范形式(你需要能够对推测路径和符号链接执行此操作)。

步骤2:要从A到B,请在A后缀足够多的“../”以向上移动目录树到公共根,然后添加字符串以向下遍历树到B。在Windows上,可能存在没有共同根的2个路径,因此并不总是可能从任何A到任何B。

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;

以下代码可以实现您想要的功能:从A开始向上查找,直到找到它和B都是后代的公共文件夹,然后再向下到达B。您可能不需要我所返回的“commonDirsInOrder”,但“routeFrom1To2”确实是您要求的。

如果您计划实际更改工作目录为“B”,那么可以直接使用“routeFrom1To2”。请注意,尽管存在“..”部分,但此函数将生成绝对路径,但这不应该是问题。


1

我需要在不使用Boost的情况下进行此操作,而其他基于std的解决方案对我来说行不通,因此我重新实现了它。当我在做这个时,我意识到我以前也做过...

无论如何,它并不像其他一些那么完整,但可能对人们有用。它是针对Windows特定的;使其符合POSIX的更改涉及字符串比较中的目录分隔符和大小写敏感性。

在我完成这个并使其工作后不久,我不得不将周围的功能转移到Python,所以所有这些都归结为 os.path.relpath(to, from)

static inline bool StringsEqual_i(const std::string& lhs, const std::string& rhs)
{
    return _stricmp(lhs.c_str(), rhs.c_str()) == 0;
}

static void SplitPath(const std::string& in_path, std::vector<std::string>& split_path)
{
    size_t start = 0;
    size_t dirsep;
    do
    {
        dirsep = in_path.find_first_of("\\/", start);
        if (dirsep == std::string::npos)
            split_path.push_back(std::string(&in_path[start]));
        else
            split_path.push_back(std::string(&in_path[start], &in_path[dirsep]));
        start = dirsep + 1;
    } while (dirsep != std::string::npos);
}

/**
 * Get the relative path from a base location to a target location.
 *
 * \param to The target location.
 * \param from The base location. Must be a directory.
 * \returns The resulting relative path.
 */
static std::string GetRelativePath(const std::string& to, const std::string& from)
{
    std::vector<std::string> to_dirs;
    std::vector<std::string> from_dirs;

    SplitPath(to, to_dirs);
    SplitPath(from, from_dirs);

    std::string output;
    output.reserve(to.size());

    std::vector<std::string>::const_iterator to_it = to_dirs.begin(),
                                             to_end = to_dirs.end(),
                                             from_it = from_dirs.begin(),
                                             from_end = from_dirs.end();

    while ((to_it != to_end) && (from_it != from_end) && StringsEqual_i(*to_it, *from_it))
    {
         ++to_it;
         ++from_it;
    }

    while (from_it != from_end)
    {
        output += "..\\";
        ++from_it;
    }

    while (to_it != to_end)
    {
        output += *to_it;
        ++to_it;

        if (to_it != to_end)
            output += "\\";
    }

    return output;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接