80

I'm writing a console utility to do some processing on files specified on the commandline, but I've run into a problem I can't solve through Google/Stack Overflow. If a full path, including drive letter, is specified, how do I reformat that path to be relative to the current working directory?

There must be something similar to the VirtualPathUtility.MakeRelative function, but if there is, it eludes me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • NDepend.Path is a fantastic library I started using for anything that involves path manipulation: https://github.com/psmacchia/NDepend.Path – Mike Marynowski Nov 06 '17 at 15:55

5 Answers5

142

If you don't mind the slashes being switched, you could [ab]use Uri:

Uri file = new Uri(@"c:\foo\bar\blop\blap.txt");
// Must end in a slash to indicate folder
Uri folder = new Uri(@"c:\foo\bar\");
string relativePath = 
Uri.UnescapeDataString(
    folder.MakeRelativeUri(file)
        .ToString()
        .Replace('/', Path.DirectorySeparatorChar)
    );

As a function/method:

string GetRelativePath(string filespec, string folder)
{
    Uri pathUri = new Uri(filespec);
    // Folders must end in a slash
    if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        folder += Path.DirectorySeparatorChar;
    }
    Uri folderUri = new Uri(folder);
    return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar));
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 2
    Nice! and just adding .Replace('/','\\') makes everything perfect – jturcotte Apr 02 '09 at 14:40
  • 13
    @total: better to use .Replace('/', Path.DirectorySeparatorChar) – Ian Kemp Aug 19 '11 at 12:56
  • 18
    Spaces in your path will become %20, you'll have to replace those as well. Perhaps best to use Uri.UnescapeDataString. Also, the last backslash of uri2 must not be omitted. – Kay Zed Oct 24 '11 at 14:48
  • 2
    Could use `Environment.CurrentDirectory` for the current directory, too. – Gaʀʀʏ Jul 17 '12 at 02:10
  • 1
    I've updated the answer to reflect the points above – joshcomley Jan 24 '13 at 00:17
  • See also http://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path/337180#337180 – CAD bloke Jun 05 '13 at 00:54
  • 4
    +1 for very good example. Apologies for the edit, but I thought an encapsulated example might be more popular/reusuable. – iCollect.it Ltd Sep 12 '13 at 08:38
  • Thanks for the useful function. I added the following line in the beginning to accommodate relative folder specs: if (!Path.IsPathRooted(folder)) folder = Path.GetFullPath(folder); – Ivan Krivyakov Jun 25 '14 at 18:54
  • This does not handle all edge cases. See [this](http://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path/32113484#32113484) answer. – Muhammad Rehan Saeed Aug 20 '15 at 10:42
  • "`if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString()))`" can be simplified to "`if (folder[folder.Length - 1] != Path.DirectorySeparatorChar)`". – RenniePet Mar 12 '18 at 07:50
  • 1
    Caution: using this in dotnet Core for linux will cause an exception raised. https://github.com/aspnet/dnx/pull/1691 – scegg May 16 '19 at 09:46
  • Caution: does not work as expected if `filespec == folder`. `GetRelativePath("c:\temp", "c:\temp")` will return `"..\temp"`. .NET Framework 4.7.2 has / .NET Standard 2.1 will finally have this as Framework method: [Path.GetRelativePath](https://learn.microsoft.com/de-de/dotnet/api/system.io.path.getrelativepath?view=netstandard-2.1) – UweB Jun 13 '19 at 04:45
  • Caution: I had problems with this approach with a file named, SchΣfer-Zimmermann2006_Chapter_RecurrentNeuralNetworksAreUniv.pdf. It generated a relative path containing Schäfer-Zimmermann2006_Chapter_RecurrentNeuralNetworksAreUniv.pdf – Ömer Cinbat Oct 07 '20 at 13:29
43

You can use Environment.CurrentDirectory to get the current directory, and FileSystemInfo.FullPath to get the full path to any location. So, fully qualify both the current directory and the file in question, and then check whether the full file name starts with the directory name - if it does, just take the appropriate substring based on the directory name's length.

Here's some sample code:

using System;
using System.IO;

class Program
{
    public static void Main(string[] args)
    {
        string currentDir = Environment.CurrentDirectory;
        DirectoryInfo directory = new DirectoryInfo(currentDir);
        FileInfo file = new FileInfo(args[0]);

        string fullDirectory = directory.FullName;
        string fullFile = file.FullName;

        if (!fullFile.StartsWith(fullDirectory))
        {
            Console.WriteLine("Unable to make relative path");
        }
        else
        {
            // The +1 is to avoid the directory separator
            Console.WriteLine("Relative path: {0}",
                              fullFile.Substring(fullDirectory.Length+1));
        }
    }
}

I'm not saying it's the most robust thing in the world (symlinks could probably confuse it) but it's probably okay if this is just a tool you'll be using occasionally.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 3
    Well, it doesn't do relative paths that are below the current directory. I imagine that's going to be fun. – Ray Mar 31 '09 at 22:38
  • Not sure what you mean: c:\Documents and Settings\Jon Skeet\Test> test.exe "c:\Documents and Settings\Jon Skeet\Test\foo\bar" Relative path: foo\bar – Jon Skeet Mar 31 '09 at 22:54
  • In other words, it works for me. Please give a concrete example of what you expect to fail. – Jon Skeet Mar 31 '09 at 22:56
  • 7
    I mean, getting a relative path for C:\TestDir\OneDirectory from C:\TestDir\AnotherDiectory is not going to return ..\OneDirectory. I'm not saying that it couldn't be changed to do that, it's just not going to be simple. – Ray Apr 01 '09 at 00:15
  • Ah, you mean paths that are *above* the current directory. No, that would be a pain. Why do you need a relative path anyway? – Jon Skeet Apr 01 '09 at 05:28
  • Ray: if you need a relative path that can go 'up' as well, you need to look up the Win32 function PathRelativePathTo. – Kay Zed Oct 24 '11 at 14:35
  • Hi Jon, how can I make it work with both directory and file? – Louis Rhys Oct 25 '11 at 08:43
  • @LouisRhys: It's not really clear what you mean, or what exactly you're trying to do. It may well be worth asking a new question (referring to this one and showing how the answers don't cut it) rather than pursuing this in comments. – Jon Skeet Oct 25 '11 at 08:47
  • @JonSkeet What I mean was, you used `FileInfo file = new FileInfo(args[0])`, meaning it's specific for a file. I wonder if it is possible to do the same when args[0] can either be a file or a directory. I didn't ask a new question because it is basically the same question as this one. – Louis Rhys Oct 25 '11 at 08:52
  • @LouisRhys: Well, you could probably use File.Exists and Directory.Exists, then create either a FileInfo or a DirectoryInfo. I don't know what happens if you try to create a FileInfo for a directory. – Jon Skeet Oct 25 '11 at 09:02
  • [Question asked here now](http://stackoverflow.com/questions/7886946/how-can-i-get-a-full-path-of-a-given-path-can-be-a-directory-or-file-or-even-f/7886974#7886974) – Paul C Oct 25 '11 at 09:04
  • @JonSkeet I also don't know, and I don't know what will happen with File.Exists(aDirectory) or Directory.Exists(a file) either.. – Louis Rhys Oct 25 '11 at 09:07
  • 2
    @LouisRhys: Well have you tried? – Jon Skeet Oct 25 '11 at 09:08
  • I did, and in my cases the exists methods return false, and FileInfo.FullPath works even though it's a directory. But I think we can't count on it unless it's documented that it will always behave like that (especially using FileInfo for a directory and vice versa) – Louis Rhys Oct 25 '11 at 09:16
10
public string MakeRelativePath(string workingDirectory, string fullPath)
{
    string result = string.Empty;
    int offset;

    // this is the easy case.  The file is inside of the working directory.
    if( fullPath.StartsWith(workingDirectory) )
    {
        return fullPath.Substring(workingDirectory.Length + 1);
    }

    // the hard case has to back out of the working directory
    string[] baseDirs = workingDirectory.Split(new char[] { ':', '\\', '/' });
    string[] fileDirs = fullPath.Split(new char[] { ':', '\\', '/' });

    // if we failed to split (empty strings?) or the drive letter does not match
    if( baseDirs.Length <= 0 || fileDirs.Length <= 0 || baseDirs[0] != fileDirs[0] )
    {
        // can't create a relative path between separate harddrives/partitions.
        return fullPath;
    }

    // skip all leading directories that match
    for (offset = 1; offset < baseDirs.Length; offset++)
    {
        if (baseDirs[offset] != fileDirs[offset])
            break;
    }

    // back out of the working directory
    for (int i = 0; i < (baseDirs.Length - offset); i++)
    {
        result += "..\\";
    }

    // step into the file path
    for (int i = offset; i < fileDirs.Length-1; i++)
    {
        result += fileDirs[i] + "\\";
    }

    // append the file
    result += fileDirs[fileDirs.Length - 1];

    return result;
}

This code is probably not bullet-proof but this is what I came up with. It's a little more robust. It takes two paths and returns path B as relative to path A.

example:

MakeRelativePath("c:\\dev\\foo\\bar", "c:\\dev\\junk\\readme.txt")
//returns: "..\\..\\junk\\readme.txt"

MakeRelativePath("c:\\dev\\foo\\bar", "c:\\dev\\foo\\bar\\docs\\readme.txt")
//returns: "docs\\readme.txt"
Ben
  • 143
  • 2
  • 6
  • 3
    I know this has been a while. But I've found a small bug. In the last for loop you use `fileDirs[offset]` this should be `fileDirs[i]` – Roy T. Aug 18 '14 at 09:00
  • This answer is my preferred one because it handles '..' relative directories, which are quickly needed. Not sure whether Roy's comment is still valid; it works at my place as it is currently displayed. – Ruud van Gaal Jan 17 '18 at 11:48
5

Thanks to the other answers here and after some experimentation I've created some very useful extension methods:

public static string GetRelativePathFrom(this FileSystemInfo to, FileSystemInfo from)
{
    return from.GetRelativePathTo(to);
}

public static string GetRelativePathTo(this FileSystemInfo from, FileSystemInfo to)
{
    Func<FileSystemInfo, string> getPath = fsi =>
    {
        var d = fsi as DirectoryInfo;
        return d == null ? fsi.FullName : d.FullName.TrimEnd('\\') + "\\";
    };

    var fromPath = getPath(from);
    var toPath = getPath(to);

    var fromUri = new Uri(fromPath);
    var toUri = new Uri(toPath);

    var relativeUri = fromUri.MakeRelativeUri(toUri);
    var relativePath = Uri.UnescapeDataString(relativeUri.ToString());

    return relativePath.Replace('/', Path.DirectorySeparatorChar);
}

Important points:

  • Use FileInfo and DirectoryInfo as method parameters so there is no ambiguity as to what is being worked with. Uri.MakeRelativeUri expects directories to end with a trailing slash.
  • DirectoryInfo.FullName doesn't normalize the trailing slash. It outputs whatever path was used in the constructor. This extension method takes care of that for you.
Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346
2

There is also a way to do this with some restrictions. This is the code from the article:

public string RelativePath(string absPath, string relTo)
    {
        string[] absDirs = absPath.Split('\\');
        string[] relDirs = relTo.Split('\\');
        // Get the shortest of the two paths 
        int len = absDirs.Length < relDirs.Length ? absDirs.Length : relDirs.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 (absDirs[index] == relDirs[index])
                lastCommonRoot = index;
            else break;
        }
        // If we didn't find a common prefix then throw 
        if (lastCommonRoot == -1)
        {
            throw new ArgumentException("Paths do not have a common base");
        }
        // Build up the relative path 
        StringBuilder relativePath = new StringBuilder();
        // Add on the .. 
        for (index = lastCommonRoot + 1; index < absDirs.Length; index++)
        {
            if (absDirs[index].Length > 0) relativePath.Append("..\\");
        }
        // Add on the folders 
        for (index = lastCommonRoot + 1; index < relDirs.Length - 1; index++)
        {
            relativePath.Append(relDirs[index] + "\\");
        }
        relativePath.Append(relDirs[relDirs.Length - 1]);
        return relativePath.ToString();
    }

When executing this piece of code:

string path1 = @"C:\Inetpub\wwwroot\Project1\Master\Dev\SubDir1"; 
string path2 = @"C:\Inetpub\wwwroot\Project1\Master\Dev\SubDir2\SubDirIWant";

System.Console.WriteLine (RelativePath(path1, path2));
System.Console.WriteLine (RelativePath(path2, path1));

it prints out:

..\SubDir2\SubDirIWant
..\..\SubDir1
Pierre Arnaud
  • 10,212
  • 11
  • 77
  • 108
Dmitry Pavlov
  • 30,789
  • 8
  • 97
  • 121