How to get relative path from absolute path

2018-12-31 16:48发布

There's a part in my apps that displays the file path loaded by the user through OpenFileDialog. It's taking up too much space to display the whole path, but I don't want to display only the filename as it might be ambiguous. So I would prefer to show the file path relative to the assembly/exe directory.

For example, the assembly resides at "C:\Program Files\Dummy Folder\MyProgram" and the file at "C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat" then I would like it to show ".\Data\datafile1.dat". If the file is in "C:\Program Files\Dummy Folder\datafile1.dat", then I would want "..\datafile1.dat". But if the file is at the root directory or 1 directory below root, then display the full path.

What solution would you recommend? Regex?

Basically I want to display useful file path info without taking too much screen space.

EDIT: Just to clarify a little bit more. The purpose of this solution is to help user or myself knowing which file did I loaded last and roughly from which directory was it from. I'm using a readonly textbox to display the path. Most of the time, the file path is much longer than the display space of the textbox. The path is supposed to be informative but not important enough as to take up more screen space.

Alex Brault comment was good, so is Jonathan Leffler. The Win32 function provided by DavidK only help with part of the problem, not the whole of it, but thanks anyway. As for James Newton-King solution, I'll give it a try later when I'm free.

标签: .net
22条回答
临风纵饮
2楼-- · 2018-12-31 17:05

If you know that toPath is contained by fromPath then you can keep it simple. I'll leave out the asserts for brevity.

public static string MakeRelativePath(string fromPath, string toPath)
{
    // use Path.GetFullPath to canonicalise the paths (deal with multiple directory seperators, etc)
    return Path.GetFullPath(toPath).Substring(Path.GetFullPath(fromPath).Length + 1);
}
查看更多
柔情千种
3楼-- · 2018-12-31 17:07

I'm using this:

public static class StringExtensions
{
  /// <summary>
  /// Creates a relative path from one file or folder to another.
  /// </summary>
  /// <param name="absPath">Absolute path.</param>
  /// <param name="relTo">Directory that defines the start of the relative path.</param> 
  /// <returns>The relative path from the start directory to the end path.</returns>
  public static string MakeRelativePath(this string absPath, string relTo)
  {
      string[] absParts = absPath.Split(Path.DirectorySeparatorChar);
      string[] relParts = relTo.Split(Path.DirectorySeparatorChar);

      // Get the shortest of the two paths
      int len = absParts.Length < relParts.Length
          ? absParts.Length : relParts.Length;

      // Use to determine where in the loop we exited
      int lastCommonRoot = -1;
      int index;

      // Find common root
      for (index = 0; index < len; index++)
      {
          if (absParts[index].Equals(relParts[index], StringComparison.OrdinalIgnoreCase))
              lastCommonRoot = index;
          else 
            break;
      }

      // If we didn't find a common prefix then throw
      if (lastCommonRoot == -1)
          throw new ArgumentException("The path of the two files doesn't have any common base.");

      // Build up the relative path
      var relativePath = new StringBuilder();

      // Add on the ..
      for (index = lastCommonRoot + 1; index < relParts.Length; index++)
      {
        relativePath.Append("..");
        relativePath.Append(Path.DirectorySeparatorChar);
      }

      // Add on the folders
      for (index = lastCommonRoot + 1; index < absParts.Length - 1; index++)
      {
        relativePath.Append(absParts[index]);
        relativePath.Append(Path.DirectorySeparatorChar);
      }
      relativePath.Append(absParts[absParts.Length - 1]);

      return relativePath.ToString();
  }
}
查看更多
君临天下
4楼-- · 2018-12-31 17:08

As Alex Brault points out, especially on Windows, the absolute path (with drive letter and all) is unambiguous and often better.

Shouldn't your OpenFileDialog use a regular tree-browser structure?

To get some nomenclature in place, the RefDir is the directory relative to which you want to specify the path; the AbsName is the absolute path name that you want to map; and the RelPath is the resulting relative path.

Take the first of these options that matches:

  • If you have different drive letters, there is no relative path from RefDir to AbsName; you must use the AbsName.
  • If the AbsName is in a sub-directory of RefDir or is a file within RefDir then simply remove the RefDir from the start of AbsName to create RelPath; optionally prepend "./" (or ".\" since you are on Windows).
  • Find the longest common prefix of RefDir and AbsName (where D:\Abc\Def and D:\Abc\Default share D:\Abc as the longest common prefix; it has to be a mapping of name components, not a simple longest common substring); call it LCP. Remove LCP from AbsName and RefDir. For each path component left in (RefDir - LCP), prepend "..\" to (AbsName - LCP) to yield RelPath.

To illustrate the last rule (which is, of course, by far the most complex), start with:

RefDir = D:\Abc\Def\Ghi
AbsName = D:\Abc\Default\Karma\Crucible

Then

LCP = D:\Abc
(RefDir - LCP) = Def\Ghi
(Absname - LCP) = Default\Karma\Crucible
RelPath = ..\..\Default\Karma\Crucible

While I was typing, DavidK produced an answer which suggests that you are not the first to need this feature and that there is a standard function to do this job. Use it. But there's no harm in being able to think your way through from first principles, either.

Except that Unix systems do not support drive letters (so everything is always located under the same root directory, and the first bullet therefore is irrelevant), the same technique could be used on Unix.

查看更多
残风、尘缘若梦
5楼-- · 2018-12-31 17:09

I'd split both of your paths at the directory level. From there, find the point of divergence and work your way back to the assembly folder, prepending a '../' everytime you pass a directory.

Keep in mind however, that an absolute path works everywhere and is usually easier to read than a relative one. I personally wouldn't show an user a relative path unless it was absolutely necessary.

查看更多
栀子花@的思念
6楼-- · 2018-12-31 17:09

The function that uses URI returned "almost" relative path. It included directory that directly contains the file which relative path I wanted to get.

Some time ago I wrote a simple function that returns relative path of folder or file, and even if it's on another drive, it includes the drive letter as well.

Please take a look:

    public static string GetRelativePath(string BasePath, string AbsolutePath)
    {
        char Separator = Path.DirectorySeparatorChar;
        if (string.IsNullOrWhiteSpace(BasePath)) BasePath = Directory.GetCurrentDirectory();
        var ReturnPath = "";
        var CommonPart = "";
        var BasePathFolders = BasePath.Split(Separator);
        var AbsolutePathFolders = AbsolutePath.Split(Separator);
        var i = 0;
        while (i < BasePathFolders.Length & i < AbsolutePathFolders.Length)
        {
            if (BasePathFolders[i].ToLower() == AbsolutePathFolders[i].ToLower())
            {
                CommonPart += BasePathFolders[i] + Separator;
            }
            else
            {
                break;
            }
            i += 1;
        }
        if (CommonPart.Length > 0)
        {
            var parents = BasePath.Substring(CommonPart.Length - 1).Split(Separator);
            foreach (var ParentDir in parents)
            {
                if (!string.IsNullOrEmpty(ParentDir))
                    ReturnPath += ".." + Separator;
            }
        }
        ReturnPath += AbsolutePath.Substring(CommonPart.Length);
        return ReturnPath;
    }
查看更多
高级女魔头
7楼-- · 2018-12-31 17:11

Way with Uri not worked on linux/macOS systems. Path '/var/www/root' can't be converted to Uri. More universal way - do all by hands.

public static string MakeRelativePath(string fromPath, string toPath, string sep = "/")
{
    var fromParts = fromPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);
    var toParts = toPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);

    var matchedParts = fromParts
        .Zip(toParts, (x, y) => string.Compare(x, y, true) == 0)
        .TakeWhile(x => x).Count();

    return string.Join("", Enumerable.Range(0, fromParts.Length - matchedParts)
        .Select(x => ".." + sep)) +
            string.Join(sep, toParts.Skip(matchedParts));
}        

PS: i use "/" as a default value of separator instead of Path.DirectorySeparatorChar, because result of this method used as uri in my app.

查看更多
登录 后发表回答