31

For example, how can I make this

"C:\RootFolder\SubFolder\MoreSubFolder\LastFolder\SomeFile.txt"

relative to this folder

"C:\RootFolder\SubFolder\"

if the expected result is

"MoreSubFolder\LastFolder\SomeFile.txt"
  • Wouldn't the expected result be "MoreSubFolder\LastFolder\SomeFile.txt"? – Chris Shain Jan 28 '12 at 04:19
  • You need to rephrase this. I read it twice, and I'm still not sure what you want really. Otherwise, I'm tempted to answer: use IndexOf and Substring methods to get the 2nd part of the path (or to remove the 1st part of the path) - and I have the feeling that's not what you want. Look into static methods of System.IO.Path class - it's got a few nice helpers for combining paths, and similar. –  Jan 28 '12 at 04:30
  • 1
    It is what I want. However, I would have preferred avoiding string manipulations. A built-in framework way would be better, if possible. –  Jan 28 '12 at 04:35
  • 1
    Does this answer your question? [How to get relative path from absolute path](https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path) – Kelly Elton Sep 04 '20 at 01:44

5 Answers5

46

Yes, you can do that, it's easy, think of your paths as URIs:

Uri fullPath = new Uri(@"C:\RootFolder\SubFolder\MoreSubFolder\LastFolder\SomeFile.txt", UriKind.Absolute);
Uri relRoot = new Uri(@"C:\RootFolder\SubFolder\", UriKind.Absolute);

string relPath = relRoot.MakeRelativeUri(fullPath).ToString();
// relPath == @"MoreSubFolder\LastFolder\SomeFile.txt"
ordag
  • 2,497
  • 5
  • 26
  • 35
15

In your example, it's simply absPath.Substring(relativeTo.Length).

More elaborate example would require going back a few levels from the relativeTo, as follows:

"C:\RootFolder\SubFolder\MoreSubFolder\LastFolder\SomeFile.txt"
"C:\RootFolder\SubFolder\Sibling\Child\"

The algorithm to make a relative path would look as follows:

  • Remove the longest common prefix (in this case, it is "C:\RootFolder\SubFolder\")
  • Count the number of folders in relativeTo (in this case, it is 2: "Sibling\Child\")
  • Insert ..\ for each remaining folder
  • Concatenate with the remainder of the absolute path after the suffix removal

The end result looks like this:

"..\..\MoreSubFolder\LastFolder\SomeFile.txt"
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
4

For modern implementations, use System.IO.Path.GetRelativePath

Path.GetRelativePath(String, String) Method
Returns a relative path from one path to another.

public static string GetRelativePath (string relativeTo, string path);

Introduced in .Net Core 2.0 (Aug 2017) and then .Net Standard 2.1 (May 2018), the implementation is very similar to the answer posted by @TarmoPikaro

Usage of this method:

string itemPath = @"C:\RootFolder\SubFolder\MoreSubFolder\LastFolder\SomeFile.txt";
string baseDirectory = @"C:\RootFolder\SubFolder\";

string result = System.IO.Path.GetRelativePath(baseDirectory, itemPath);
Console.WriteLine(result);

Results in:

MoreSubFolder\LastFolder\SomeFile.txt

As with the answer from @TarmoPikaro, this implementation makes use of System.IO.Path.GetFullPath to resolve potentially relative paths passed through before comparison.

The intention behind this is to resolve paths that are constructed by appending a base path with a relative path which we would often do in our code before calling GetRelativePath(). It should resolve the following:

"c:\test\..\test2" => "c:\test2"

Which is expected, but if the input paths are fully relative, the path will be resolved to the current working folder, in my test app this looks like this:

".\test2" => "D:\Source\Repos\MakeRoot\bin\Debug\net6.0\test2"

In most cases this will lead to unexpected results from MakeRelative. For this reason it is actually expected that you resolve the input arguments using concatenation or your own call to GetFullPath first.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
2

After searching for makeRelative in following git repository: https://github.com/tapika/syncProj/blob/8ea41ebc11f538a22ed7cfaf59a8b7e0b4c3da37/syncProj.cs#L1685

We find this solution, which has been enhanced with documentation after a bit of testing ;)

public static partial class PathUtilities
{
    /// <summary>
    /// Rebases file with path <paramref name="fullPath"/> to folder with <paramref name="baseDir"/>.
    /// </summary>
    /// <param name="fullPath">Full file path (absolute)</param>
    /// <param name="baseDir">Full base directory path (absolute)</param>
    /// <returns>Relative path to file with respect to <paramref name="baseDir"/></returns>
    /// <remarks>Paths are resolved by calling the <seealso cref="System.IO.Path.GetFullPath(string)"/> method before calculating the difference. This will flatten relative path fragments:
    /// <code>
    /// "c:\test\..\test2" => "c:\test2"
    /// </code>
    /// These path framents are expected to be created by concatenating a root folder with a relative path such as this:
    /// <code>
    /// var baseFolder = @"c:\test\";
    /// var virtualPath = @"..\test2";
    /// var fullPath = System.IO.Path.Combine(baseFolder, virtualPath);
    /// </code>
    /// The default file path for the current executing environment will be used for the base resolution for this operation, which may not be appropriate if the input paths are fully relative or relative to different
    /// respective base paths. For this reason we should attempt to resolve absolute input paths <i>before</i> passing through as arguments to this method.
    /// </remarks>
    static public string MakeRelative(string fullPath, string baseDir)
    {
        String pathSep = "\\";
        String itemPath = Path.GetFullPath(fullPath);
        String baseDirPath = Path.GetFullPath(baseDir); // If folder contains upper folder references, they get resolved here. "c:\test\..\test2" => "c:\test2"

        String[] p1 = Regex.Split(itemPath, "[\\\\/]").Where(x => x.Length != 0).ToArray();
        String[] p2 = Regex.Split(baseDir, "[\\\\/]").Where(x => x.Length != 0).ToArray();
        int i = 0;

        for (; i < p1.Length && i < p2.Length; i++)
            if (String.Compare(p1[i], p2[i], true) != 0)    // Case insensitive match
                break;

        if (i == 0)     // Cannot make relative path, for example if resides on different drive
            return itemPath;

        String r = String.Join(pathSep, Enumerable.Repeat("..", p2.Length - i).Concat(p1.Skip(i).Take(p1.Length - i)));
        return r;
    }
}

Usage of this method:

string itemPath = @"C:\RootFolder\SubFolder\MoreSubFolder\LastFolder\SomeFile.txt";
string baseDirectory = @"C:\RootFolder\SubFolder\";

string result = PathUtilities.MakeRelative(itemPath, baseDirectory);
Console.WriteLine(result);

Results in:

MoreSubFolder\LastFolder\SomeFile.txt
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
TarmoPikaro
  • 4,723
  • 2
  • 50
  • 62
  • Sorry, but this does not seem to work `makeRelative(@"F:\a\b", @"F:\")` for example returns `..\b` (instead of `a\b`) – Mikescher Mar 01 '17 at 14:23
  • Can you try again - I have updated my latest version ? If it does not work, I'll fix it. – TarmoPikaro Mar 01 '17 at 16:05
  • Provide full code, do not make link answer only. The code has changed and your link points to another part of the code. – xmedeko Mar 22 '19 at 18:00
  • Post has been updated to include better information in the doc comments, @TarmoPikaro well done, this is still a good solution for .Net Framework code. – Chris Schaller Sep 19 '22 at 03:17
2

.NET Core and .NET provide System.IO.Path.GetRelativePath(string, string) method in the standard library. If you need to use that method in an older .NET Framework project then you can use the following polyfill which closely mimicks the standard BCL behavior:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

static class PathUtil
{
    public static string GetRelativePath(string relativeTo, string path)
    {
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
        return Path.GetRelativePath(relativeTo, path);
#else
        return GetRelativePathPolyfill(relativeTo, path);
#endif
    }

#if !(NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER)
    static string GetRelativePathPolyfill(string relativeTo, string path)
    {
        path = Path.GetFullPath(path);
        relativeTo = Path.GetFullPath(relativeTo);

        var separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
        IReadOnlyList<string> p1 = path.Split(separators);
        IReadOnlyList<string> p2 = relativeTo.Split(separators, StringSplitOptions.RemoveEmptyEntries);

        var sc = StringComparison;

        int i;
        int n = Math.Min(p1.Count, p2.Count);
        for (i = 0; i < n; i++)
            if (!string.Equals(p1[i], p2[i], sc))
                break;

        if (i == 0)
        {
            // Cannot make a relative path, for example if the path resides on another drive.
            return path;
        }

        p1 = p1.Skip(i).Take(p1.Count - i).ToList();

        if (p1.Count == 1 && p1[0].Length == 0)
            p1 = Array.Empty<string>();

        string relativePath = string.Join(
            new string(Path.DirectorySeparatorChar, 1),
            Enumerable.Repeat("..", p2.Count - i).Concat(p1));

        if (relativePath.Length == 0)
            relativePath = ".";

        return relativePath;
    }

    static StringComparison StringComparison =>
        IsCaseSensitive ?
            StringComparison.Ordinal :
            StringComparison.OrdinalIgnoreCase;

    static bool IsCaseSensitive =>
        !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
        RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
#endif
}

The code above covers quite a few edge cases in order to provide the same behavior as System.IO.Path.GetRelativePath(string, string) method.

ogggre
  • 2,204
  • 1
  • 23
  • 19