Getting path relative to the current working directory? [duplicate]
Asked Answered
C

5

83

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.

Charissa answered 31/3, 2009 at 22:9 Comment(1)
NDepend.Path is a fantastic library I started using for anything that involves path manipulation: github.com/psmacchia/NDepend.PathFingering
A
143

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));
}
Atmometer answered 31/3, 2009 at 22:15 Comment(13)
Nice! and just adding .Replace('/','\\') makes everything perfectNonpayment
@total: better to use .Replace('/', Path.DirectorySeparatorChar)Blakeney
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.Footman
Could use Environment.CurrentDirectory for the current directory, too.Monro
I've updated the answer to reflect the points aboveSarmentose
See also #276189Charqui
+1 for very good example. Apologies for the edit, but I thought an encapsulated example might be more popular/reusuable.Procaine
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);Haemophilic
This does not handle all edge cases. See this answer.Scherle
"if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString()))" can be simplified to "if (folder[folder.Length - 1] != Path.DirectorySeparatorChar)".Concrete
Caution: using this in dotnet Core for linux will cause an exception raised. github.com/aspnet/dnx/pull/1691Galliard
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.GetRelativePathRepertory
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.pdfTavis
M
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.

Merovingian answered 31/3, 2009 at 22:14 Comment(14)
Well, it doesn't do relative paths that are below the current directory. I imagine that's going to be fun.Homunculus
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\barMerovingian
In other words, it works for me. Please give a concrete example of what you expect to fail.Merovingian
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.Homunculus
Ah, you mean paths that are above the current directory. No, that would be a pain. Why do you need a relative path anyway?Merovingian
Ray: if you need a relative path that can go 'up' as well, you need to look up the Win32 function PathRelativePathTo.Footman
Hi Jon, how can I make it work with both directory and file?Motel
@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.Merovingian
@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.Motel
@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.Merovingian
Question asked here nowMouflon
@JonSkeet I also don't know, and I don't know what will happen with File.Exists(aDirectory) or Directory.Exists(a file) either..Motel
@LouisRhys: Well have you tried?Merovingian
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)Motel
C
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"
Conover answered 18/10, 2013 at 15:37 Comment(2)
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]Kristopherkristos
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.Punchdrunk
I
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.
Ideo answered 16/5, 2014 at 14:1 Comment(0)
C
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
Carnegie answered 23/5, 2012 at 15:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.