How to find relative path given two absolute paths?

吃可爱长大的小学妹 提交于 2019-11-29 10:45:21
Thomas Dickey

I answered a similar question here: Resolving a relative path without referencing the current directory on Windows.

There is no standard function for this. There is a function in vi-like-emacs for this purpose. A quick check of apropos relative shows me few other programs which likely implement this: revpath for example).

It could be done as a string-manipulation (no need to compute working directories):

  • start by finding the longest common prefix which ends with a path-separator.
  • if there is no common prefix, you are done
  • strip the common prefix from (a copy of...) the current and target strings
  • replace each directory-name in the current string with ".."
  • add that (with a path-separator) in front of the target string
  • return that combined string

The "done" in the second step presumes that you want to use a relative path to shorten the result. On the other hand, you might want to use a relative pathname regardless of the length. In that case, just skip the step (the result will be longer, but relative).

Caleb

Find the longest common path (in this case, /a/path/to) and delete it from both absolute paths. That would give:

  • /a
  • /somewhere/else

Now, replace each path component in the starting path with ../ and prepend the result to the destination path. If you want to go from directory else to directory a, that would give you:

../../a

If you want to go the other way, you'd instead have:

../somewhere/else

Build a tree with the first absolute path, then add the second path to that tree, and then walk from one leaf to the other: a step from one node to its parent is translated to a "../" sequence, and a step from a node to one of its children is translated to the name of that children. Notice that there might be more than one solution. For example:

1) /a/path/to/a

And

2) /a/path/to/a/new/one

The obvious path from (1) to (2) is new/one but ../../../a/path/to/a/new/one is also valid. When you write the algorithm to do the walking in your tree you have to be aware of this

This is implemented in the "ln" command, part of the GNU Coreutils package (for ln -r). Obviously there are many ways to go about this, and one could even derive some benefit from coming up with a solution oneself, without looking at existing code. Personally I find the code in Coreutils to be rather instructive.

If I had to convert absolute paths to a relative path in a C project, I would just copy "relpath.c" from Coreutils. It has a few dependencies to other utility functions in the package, these would need to be brought in in some form as well. Here is the main "relpath()" function. Note that it works on canonicalized pathnames, for example it doesn't like for paths to contain stuff like "//" or "/.". Basically it finds the common prefix of the two paths, then it treats the four cases which arise depending on whether the remaining part of each path is empty or not.

/* Output the relative representation if possible.
   If BUF is non-NULL, write to that buffer rather than to stdout.  */
bool
relpath (const char *can_fname, const char *can_reldir, char *buf, size_t len)
{
  bool buf_err = false;

  /* Skip the prefix common to --relative-to and path.  */
  int common_index = path_common_prefix (can_reldir, can_fname);
  if (!common_index)
    return false;

  const char *relto_suffix = can_reldir + common_index;
  const char *fname_suffix = can_fname + common_index;

  /* Skip over extraneous '/'.  */
  if (*relto_suffix == '/')
    relto_suffix++;
  if (*fname_suffix == '/')
    fname_suffix++;

  /* Replace remaining components of --relative-to with '..', to get
     to a common directory.  Then output the remainder of fname.  */
  if (*relto_suffix)
    {
      buf_err |= buffer_or_output ("..", &buf, &len);
      for (; *relto_suffix; ++relto_suffix)
        {
          if (*relto_suffix == '/')
            buf_err |= buffer_or_output ("/..", &buf, &len);
        }

      if (*fname_suffix)
        {
          buf_err |= buffer_or_output ("/", &buf, &len);
          buf_err |= buffer_or_output (fname_suffix, &buf, &len);
        }
    }
  else
    {
        buf_err |= buffer_or_output (*fname_suffix ? fname_suffix : ".",
                                     &buf, &len);
    }

  if (buf_err)
    error (0, ENAMETOOLONG, "%s", _("generating relative path"));

  return !buf_err;
}

You can view the rest of "relpath.c" here. There is a higher-level wrapper function in "ln.c" which canonicalizes its path name arguments before calling relpath, it is named convert_abs_rel(). That's probably what you want to be calling most of the time.

Using cwalk you can use cwk_path_get_relative, which even works cross-platform:

#include <cwalk.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  char buffer[FILENAME_MAX];

  cwk_path_get_relative("/hello/there/", "/hello/world", buffer, sizeof(buffer));
  printf("The relative path is: %s", buffer);

  return EXIT_SUCCESS;
}

Output:

The relative path is: ../world
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!