Getting path relative to the current working directory?

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.

220367 次浏览

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.

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

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
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"

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.