【问题标题】:Get relative path from two absolute paths从两个绝对路径获取相对路径
【发布时间】:2011-08-11 23:51:07
【问题描述】:

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

用例:

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

boost::filesystem 似乎有 complete 来解析 relative ~ relative => absolute,但没有反向执行此操作 (absolute ~ absolute => relative)。

我想用 Boost 路径来做。

【问题讨论】:

标签: c++ boost boost-filesystem


【解决方案1】:

从版本1.60.0 boost.filesystem 开始支持这一点。您正在寻找成员函数path lexically_relative(const path& p) const

下面是 1.60.0 之前的原始答案。


Boost 不支持这个;这是一个悬而未决的问题——#1976 (Inverse function for complete)——但似乎并没有引起太大的关注。

这是一个模糊的、幼稚的解决方法,似乎可以解决问题(不确定是否可以改进):

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

【讨论】:

  • 感谢分享。我也花了一些时间寻找这个缺失的功能
  • 由于使用了点和斜线,此代码不适用于 boost::filesystem 的第 3 版,但经过一些修改后它可以工作。
  • @Phineas:作为答案发布的修改代码将不胜感激:)
  • 我写评论时没有工作代码。用点和斜线注释掉使用的行,然后将_dot、_dots 和_sep 分别设置为“.”、“..”和“/”。我不确定 V3 中的点和斜线发生了什么或是否有替代品。
  • @Phineas:我认为这将是一个倒退。我故意将它们抽象化(尽管无可否认,很糟糕)。如果 dotslash 在 v3 中已被删除,那么我想了解原因,我想找出应该用什么抽象机制来代替它们。
【解决方案2】:

我只是在考虑使用 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"

它就像一个魅力,拯救了我几个小时的生命。

【讨论】:

    【解决方案3】:

    我刚刚编写了可以将绝对路径转换为相对路径的代码。它适用于我所有的用例,但我不能保证它是完美的。

    为了便于阅读,我将 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。此实现将为 relativePath("/home/adam/file", "/home/ulrik") 返回 "adam/file"。似乎实现需要一个尾随“/”,但例如 path("/home/ulrik/file").parent_path() 没有这样的尾随斜杠。我认为应该删除“if (itr_relative_to)”。
    • 对我来说,这个relativePath() 在某些情况下会退出,因为{itr_path, itr_relative_to} 迭代器之一可能已经等于它们各自的.end()。在这种情况下,无法进行比较 *itr_path==*itr_relative_to。在... != .end() 检查之后移动比较可以解决这个问题。
    • 谢谢@applesoup,我已更改代码以反映您的评论。
    【解决方案4】:

    我已经为这个技巧写了一个简单的解决方案。 没有使用 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;                                                              
    }
    

    【讨论】:

    • /n?有人对替换按钮感到满意。 (固定)
    【解决方案5】:

    这是我在 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”。请注意,尽管有所有“..”部分,此函数仍会生成绝对路径,但这应该不是问题。

    【讨论】:

      【解决方案6】:

      我需要在没有 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;
      }
      

      【讨论】:

        【解决方案7】:

        使用 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引入的)在比较之前。

        【讨论】:

        • 喜欢这些例子:D 很高兴标准化版本得到了,如果你能原谅双关语,完成
        猜你喜欢
        • 1970-01-01
        • 2022-06-10
        • 2021-11-08
        • 2013-10-27
        • 2010-09-21
        • 2023-03-22
        • 1970-01-01
        相关资源
        最近更新 更多