199

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.

Oneiros
  • 4,328
  • 6
  • 40
  • 69
faulty
  • 8,117
  • 12
  • 44
  • 61

24 Answers24

213

.NET Core 2.0 has Path.GetRelativePath, else, use this.

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path or <c>toPath</c> if the paths are not related.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static String MakeRelativePath(String fromPath, String toPath)
{
    if (String.IsNullOrEmpty(fromPath)) throw new ArgumentNullException("fromPath");
    if (String.IsNullOrEmpty(toPath))   throw new ArgumentNullException("toPath");

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

    if (fromUri.Scheme != toUri.Scheme) { return toPath; } // path can't be made relative.

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

    if (toUri.Scheme.Equals("file", StringComparison.InvariantCultureIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}
Ramon Smits
  • 2,482
  • 1
  • 18
  • 20
  • 35
    After lots of testing this method worked best for me. You need to remember that Uri treats a folder that doesn't end with a path separator as a file (use c:\foo\bar\ instead of c:\foo\bar if bar is a folder). – VVS Jun 02 '09 at 12:12
  • 2
    It will change backslashes into slashes, right? Otherwise it works fine for me! – Stiefel Sep 16 '10 at 13:25
  • 6
    A general solution for the slashes problem is to use `return relativeUri.ToString().Replace('/',Path.DirectorySeparatorChar);` – Nyerguds Mar 15 '11 at 12:59
  • 4
    You should unescape the relative uri thus created to get a valid path; the .ToString() representation will include escape sequences that aren't valid and aren't necessary in the path. – Eamon Nerbonne Jun 05 '11 at 19:01
  • 3
    added the below after arg checks if (fromPath.Last() != Path.DirectorySeparatorChar) { fromPath += Path.DirectorySeparatorChar; } if (toPath.Last() != Path.DirectorySeparatorChar) { toPath += Path.DirectorySeparatorChar; } – Simon Aug 08 '12 at 10:06
  • 1
    This would probably not work in case of one long file name and one short file name. But that would require filename-normalization, and then you would have to assume/require/check that the path does indeed exist. – Yahoo Serious Mar 07 '13 at 15:00
  • 1
    This is problematic, because it assumes you're only working with files on disk, and not URLs, etc. -- you should probably check to see what kind of slashes are being used in the first place before just randomly replacing slashes... – BrainSlugs83 Nov 08 '13 at 20:31
  • 1
    Suggested an edit to fix the above issue -- it checks if the .Scheme.ToUpperInvariant() == "FILE" before replacing slashes. – BrainSlugs83 Nov 08 '13 at 20:51
  • 9
    For me this is **not** returning the relative path. For `c:\test` and `c:\test\abc.txt` it returns `test\abc.txt` which is not relative in my opinion. I would expect just `abc.txt` – juergen d Mar 05 '15 at 14:26
  • 3
    @juergend: I would expect `.\abc.txt` on a Windows system. – nategoose Mar 20 '15 at 13:56
  • 3
    Note that this code now fails .Net 4.5 when the `toPath` contains an escaped letter. For example `MakeRelativePath(@"c:\root\", @"c:\root\%74%65%73%74\filename.txt")` returns `test\filename.txt` instead of `%74%65%73%74\filename.txt`. Not a common scenario, but it is possible. – Daws May 29 '15 at 20:21
  • 4
    @juergend: the folder path need to end with \ to be consider a folder. In your case, 'c:\test\' and 'c:\test\abc.txt' will produce correct result. It's kind of hacky though. – Hoàng Long Jul 05 '16 at 08:59
  • As far as I can tell this doesn't insert the `.` or `..`'s – Emperor Eto Dec 15 '21 at 20:09
  • Is there a variant of this function which behaves really similar to `Path.GetRelativePath(...)` regarding the trailing slashes. In my case, I often don't know if the path is a file or a directory. If I append a slash to `toPath` (as this always is a directory), the function will fail if `fromPath` and `toPath` are equal (except for the slash). However, appending a slash to `fromPath` will not make sense if it is a file... – LionAM Dec 20 '21 at 13:37
58

.NET Core 2.0 Answer

.NET Core 2.0 has Path.GetRelativePath which can be used like so:

var relativePath = Path.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

In the above example, the relativePath variable is equal to Data\datafile1.dat.

Alternative .NET Answer

@Dave's solution does not work when the file paths do not end with a forward slash character (/) which can happen if the path is a directory path. My solution fixes that problem and also makes use of the Uri.UriSchemeFile constant instead of hard coding "FILE".

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fromPath"/> or <paramref name="toPath"/> is <c>null</c>.</exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static string GetRelativePath(string fromPath, string toPath)
{
    if (string.IsNullOrEmpty(fromPath))
    {
        throw new ArgumentNullException("fromPath");
    }

    if (string.IsNullOrEmpty(toPath))
    {
        throw new ArgumentNullException("toPath");
    }

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

    if (fromUri.Scheme != toUri.Scheme)
    {
        return toPath;
    }

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

    if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}

private static string AppendDirectorySeparatorChar(string path)
{
    // Append a slash only if the path is a directory and does not have a slash.
    if (!Path.HasExtension(path) &&
        !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        return path + Path.DirectorySeparatorChar;
    }

    return path;
}

Windows Interop Answer

There is a Windows API called PathRelativePathToA that can be used to find a relative path. Please note that the file or directory paths that you pass to the function must exist for it to work.

var relativePath = PathExtended.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

public static class PathExtended
{
    private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
    private const int FILE_ATTRIBUTE_NORMAL = 0x80;
    private const int MaximumPath = 260;

    public static string GetRelativePath(string fromPath, string toPath)
    {
        var fromAttribute = GetPathAttribute(fromPath);
        var toAttribute = GetPathAttribute(toPath);

        var stringBuilder = new StringBuilder(MaximumPath);
        if (PathRelativePathTo(
            stringBuilder,
            fromPath,
            fromAttribute,
            toPath,
            toAttribute) == 0)
        {
            throw new ArgumentException("Paths must have a common prefix.");
        }

        return stringBuilder.ToString();
    }

    private static int GetPathAttribute(string path)
    {
        var directory = new DirectoryInfo(path);
        if (directory.Exists)
        {
            return FILE_ATTRIBUTE_DIRECTORY;
        }

        var file = new FileInfo(path);
        if (file.Exists)
        {
            return FILE_ATTRIBUTE_NORMAL;
        }

        throw new FileNotFoundException(
            "A file or directory with the specified path was not found.",
            path);
    }

    [DllImport("shlwapi.dll", SetLastError = true)]
    private static extern int PathRelativePathTo(
        StringBuilder pszPath,
        string pszFrom,
        int dwAttrFrom,
        string pszTo,
        int dwAttrTo);
}
Muhammad Rehan Saeed
  • 35,627
  • 39
  • 202
  • 311
  • 1
    Works much more like what one would expect - I recommend this instead of the highest-upvoted answer. – theMayer Nov 06 '18 at 23:05
  • Seeing as you assume `AltDirectorySeparatorChar` is a possibility, shouldn't `AppendDirectorySeparatorChar` check for it too? – Ohad Schneider Sep 05 '19 at 11:28
  • Also, a file could be without an extension, so while this may be more convenient in most cases, it doesn't allow you to specify that case. Maybe add a check if the file system entry exists, and if so check whether its a file or a folder. If it doesn't exist, stay with this logic. Or maybe even add some way in the signature to specify whether a file or directory have been provided (e.g. 2 booleans). – Ohad Schneider Sep 05 '19 at 11:30
  • Finally, I would throw an exception if the schemes are different. – Ohad Schneider Sep 05 '19 at 11:35
  • I found that the "Alternative .NET Answer" as written returns a file URI rather than a path (e.g. "file:///c:/etc") if the paths are not related. To fix this, I changed line 30 in the code sample to: `string relativePath = relativeUri.IsAbsoluteUri ? relativeUri.LocalPath : Uri.UnescapeDataString(relativeUri.ToString());` – Eric Pohl Jan 20 '22 at 16:02
  • In the alternative .NET example !Path.HasExtension(path) is not a good way to determine if something is a folder (I ran into this trying to find something relative a folder net6.0). A better way to determine if something is a folder is to look at its attributes. `(File.GetAttributes(path) & FileAttributes.Directory) == FileAttributes.Directory` – bobbyg603 Nov 28 '22 at 22:44
53

A bit late to the question, but I just needed this feature as well. I agree with DavidK that since there is a built-in API function that provides this, you should use it. Here's a managed wrapper for it:

public static string GetRelativePath(string fromPath, string toPath)
{
    int fromAttr = GetPathAttribute(fromPath);
    int toAttr = GetPathAttribute(toPath);

    StringBuilder path = new StringBuilder(260); // MAX_PATH
    if(PathRelativePathTo(
        path,
        fromPath,
        fromAttr,
        toPath,
        toAttr) == 0)
    {
        throw new ArgumentException("Paths must have a common prefix");
    }
    return path.ToString();
}

private static int GetPathAttribute(string path)
{
    DirectoryInfo di = new DirectoryInfo(path);
    if (di.Exists)
    {
        return FILE_ATTRIBUTE_DIRECTORY;
    }

    FileInfo fi = new FileInfo(path);
    if(fi.Exists)
    {
        return FILE_ATTRIBUTE_NORMAL;
    }

    throw new FileNotFoundException();
}

private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
private const int FILE_ATTRIBUTE_NORMAL = 0x80;

[DllImport("shlwapi.dll", SetLastError = true)]
private static extern int PathRelativePathTo(StringBuilder pszPath, 
    string pszFrom, int dwAttrFrom, string pszTo, int dwAttrTo);
ctacke
  • 66,480
  • 18
  • 94
  • 155
  • 4
    I wouldn't throw an exception if the file or path doesn't exist since this could be a totally legal case. – VVS Jun 02 '09 at 12:01
  • 2
    So what would GetPathAttributes return then? There is no flag for "file doesn't exist" so I don't see any viable option other than to throw, otherwise the caller gets erroneous info. – ctacke Jun 02 '09 at 13:05
  • 2
    Note that PathRelativePathTo returns FALSE if no relative path could be created. In that case you should either return String.Empty or throw an exception. – Daniel Rose Jun 21 '10 at 11:27
  • 2
    @ctacke: You could also P/Invoke with a return type of bool and set [return: MarshalAs(UnmanagedType.Bool)]. – Daniel Rose Jun 21 '10 at 14:40
  • 1
    @daniel: sure, but there's no real benefit to clarity in doing so. – ctacke Jun 21 '10 at 16:23
  • 4
    I find it clearer: It allows code such as bool success = PathRelativePathTo(...) which I find easier to understand than an int where you need to read the documentation on what the int means. – Daniel Rose Jun 22 '10 at 06:32
  • 1
    This does not work so well with Network Shares that aren't reachable at the moment. – juergen d Jul 23 '15 at 21:39
  • 2
    this is a crappy function, it doesn't work in case of non existent paths. it doesnt work with forward slashes... apparently above Uri based technique seems more robust. – v.oddou Aug 06 '15 at 06:57
  • 4
    People... you can just _remove_ the whole GetPathAttribute, you know. As long as you make absolutely sure the arguments you give are directories, you just need to give it 0x10 and it'll work with completely nonexistent paths. And in my case, the preferred solution is simply to return the full absolute target path instead of throwing that exception. – Nyerguds Feb 04 '16 at 20:31
25

There is a Win32 (C++) function in shlwapi.dll that does exactly what you want: PathRelativePathTo()

I'm not aware of any way to access this from .NET other than to P/Invoke it, though.

Liam
  • 27,717
  • 28
  • 128
  • 190
DavidK
  • 3,929
  • 1
  • 19
  • 26
  • this function only simplified part of the problem. – faulty Nov 11 '08 at 07:58
  • 3
    Which part doesn't it help with? Reading the original post it looks to me like PathRelativePathTo() does what you wanted, but that's probably becuase I've misinterpred something ... – DavidK Nov 11 '08 at 15:12
  • 3
    Works flawlessly. See http://pinvoke.net/default.aspx/shlwapi.PathRelativePathTo on how to setup the P/Invoke. – joce Apr 20 '11 at 04:28
  • 2
    Thank you! I was actually looking for a C++ solution! – NTDLS Jul 21 '11 at 14:00
  • 2
    It's worth noting that functions in `shlwapi.dll` are now deprecated http://msdn.microsoft.com/en-us/library/windows/desktop/bb759845(v=vs.85).aspx `"These functions are available through Windows XP Service Pack 2 (SP2) and Windows Server 2003. They might be altered or unavailable in subsequent versions of Windows."` – Basic May 19 '14 at 07:10
  • 4
    I did not read that page as stating that shlwapi.dll itself is deprecated: all it says is that the wrapper functions from shlwapi.dll listed on the page are deprecated. PathRelativePathTo() itself is not mentioned on that page, and the main documentation for PathRelativePathTo() makes no mention of deprecation, so as far as I can see it is still a valid function to call. – DavidK May 20 '14 at 14:46
14

If you are using .NET Core 2.0, Path.GetRelativePath() is available providing this specific functionality:

        var relativeTo = @"C:\Program Files\Dummy Folder\MyProgram";
        var path = @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat";

        string relativePath = System.IO.Path.GetRelativePath(relativeTo, path);

        System.Console.WriteLine(relativePath);
        // output --> Data\datafile1.dat 

Otherwise, for .NET full framework (as of v4.7) recommend using one of the other suggested answers.

Ray
  • 187,153
  • 97
  • 222
  • 204
9

I have used this in the past.

/// <summary>
/// Creates a relative path from one file
/// or folder to another.
/// </summary>
/// <param name="fromDirectory">
/// Contains the directory that defines the
/// start of the relative path.
/// </param>
/// <param name="toPath">
/// Contains the path that defines the
/// endpoint of the relative path.
/// </param>
/// <returns>
/// The relative path from the start
/// directory to the end path.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public static string MakeRelative(string fromDirectory, string toPath)
{
  if (fromDirectory == null)
    throw new ArgumentNullException("fromDirectory");

  if (toPath == null)
    throw new ArgumentNullException("toPath");

  bool isRooted = (Path.IsPathRooted(fromDirectory) && Path.IsPathRooted(toPath));

  if (isRooted)
  {
    bool isDifferentRoot = (string.Compare(Path.GetPathRoot(fromDirectory), Path.GetPathRoot(toPath), true) != 0);

    if (isDifferentRoot)
      return toPath;
  }

  List<string> relativePath = new List<string>();
  string[] fromDirectories = fromDirectory.Split(Path.DirectorySeparatorChar);

  string[] toDirectories = toPath.Split(Path.DirectorySeparatorChar);

  int length = Math.Min(fromDirectories.Length, toDirectories.Length);

  int lastCommonRoot = -1;

  // find common root
  for (int x = 0; x < length; x++)
  {
    if (string.Compare(fromDirectories[x], toDirectories[x], true) != 0)
      break;

    lastCommonRoot = x;
  }

  if (lastCommonRoot == -1)
    return toPath;

  // add relative folders in from path
  for (int x = lastCommonRoot + 1; x < fromDirectories.Length; x++)
  {
    if (fromDirectories[x].Length > 0)
      relativePath.Add("..");
  }

  // add to folders to path
  for (int x = lastCommonRoot + 1; x < toDirectories.Length; x++)
  {
    relativePath.Add(toDirectories[x]);
  }

  // create relative path
  string[] relativeParts = new string[relativePath.Count];
  relativePath.CopyTo(relativeParts, 0);

  string newPath = string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts);

  return newPath;
}
James Newton-King
  • 48,174
  • 24
  • 109
  • 130
  • 1
    I'll look into it, I need sometime to test it out. Thanks – faulty Nov 11 '08 at 08:00
  • 1
    I would suggest to use Path.GetFullPath() to successfully compare two paths with relative bits. Example: c:\a\..\b vs. c:\b vs. c:\b\.\ – VVS Jun 02 '09 at 11:43
8

As pointed above .NET Core 2.x has implementation of Path.GetRelativePath.

Code below is adapted from sources and works fine with .NET 4.7.1 Framework.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

//Adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Path.cs#L697
// by Anton Krouglov

using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Text;
using Xunit;

namespace System.IO {
    // Provides methods for processing file system strings in a cross-platform manner.
    // Most of the methods don't do a complete parsing (such as examining a UNC hostname), 
    // but they will handle most string operations.
    public static class PathNetCore {

        /// <summary>
        /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
        /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
        /// </summary>
        /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
        /// <param name="path">The destination path.</param>
        /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
        public static string GetRelativePath(string relativeTo, string path) {
            return GetRelativePath(relativeTo, path, StringComparison);
        }

        private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) {
            if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
            Debug.Assert(comparisonType == StringComparison.Ordinal ||
                         comparisonType == StringComparison.OrdinalIgnoreCase);

            relativeTo = Path.GetFullPath(relativeTo);
            path = Path.GetFullPath(path);

            // Need to check if the roots are different- if they are we need to return the "to" path.
            if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType))
                return path;

            int commonLength = PathInternalNetCore.GetCommonPathLength(relativeTo, path,
                ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);

            // If there is nothing in common they can't share the same root, return the "to" path as is.
            if (commonLength == 0)
                return path;

            // Trailing separators aren't significant for comparison
            int relativeToLength = relativeTo.Length;
            if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo))
                relativeToLength--;

            bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path);
            int pathLength = path.Length;
            if (pathEndsInSeparator)
                pathLength--;

            // If we have effectively the same path, return "."
            if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";

            // We have the same root, we need to calculate the difference now using the
            // common Length and Segment count past the length.
            //
            // Some examples:
            //
            //  C:\Foo C:\Bar L3, S1 -> ..\Bar
            //  C:\Foo C:\Foo\Bar L6, S0 -> Bar
            //  C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
            //  C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar

            StringBuilder
                sb = new StringBuilder(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));

            // Add parent segments for segments past the common on the "from" path
            if (commonLength < relativeToLength) {
                sb.Append("..");

                for (int i = commonLength + 1; i < relativeToLength; i++) {
                    if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) {
                        sb.Append(DirectorySeparatorChar);
                        sb.Append("..");
                    }
                }
            }
            else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) {
                // No parent segments and we need to eat the initial separator
                //  (C:\Foo C:\Foo\Bar case)
                commonLength++;
            }

            // Now add the rest of the "to" path, adding back the trailing separator
            int differenceLength = pathLength - commonLength;
            if (pathEndsInSeparator)
                differenceLength++;

            if (differenceLength > 0) {
                if (sb.Length > 0) {
                    sb.Append(DirectorySeparatorChar);
                }

                sb.Append(path, commonLength, differenceLength);
            }

            return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb);
        }

        // Public static readonly variant of the separators. The Path implementation itself is using
        // internal const variant of the separators for better performance.
        public static readonly char DirectorySeparatorChar = PathInternalNetCore.DirectorySeparatorChar;
        public static readonly char AltDirectorySeparatorChar = PathInternalNetCore.AltDirectorySeparatorChar;
        public static readonly char VolumeSeparatorChar = PathInternalNetCore.VolumeSeparatorChar;
        public static readonly char PathSeparator = PathInternalNetCore.PathSeparator;

        /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
        internal static StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
    }

    /// <summary>Contains internal path helpers that are shared between many projects.</summary>
    internal static class PathInternalNetCore {
        internal const char DirectorySeparatorChar = '\\';
        internal const char AltDirectorySeparatorChar = '/';
        internal const char VolumeSeparatorChar = ':';
        internal const char PathSeparator = ';';

        internal const string ExtendedDevicePathPrefix = @"\\?\";
        internal const string UncPathPrefix = @"\\";
        internal const string UncDevicePrefixToInsert = @"?\UNC\";
        internal const string UncExtendedPathPrefix = @"\\?\UNC\";
        internal const string DevicePathPrefix = @"\\.\";

        //internal const int MaxShortPath = 260;

        // \\?\, \\.\, \??\
        internal const int DevicePrefixLength = 4;

        /// <summary>
        /// Returns true if the two paths have the same root
        /// </summary>
        internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) {
            int firstRootLength = GetRootLength(first);
            int secondRootLength = GetRootLength(second);

            return firstRootLength == secondRootLength
                   && string.Compare(
                       strA: first,
                       indexA: 0,
                       strB: second,
                       indexB: 0,
                       length: firstRootLength,
                       comparisonType: comparisonType) == 0;
        }

        /// <summary>
        /// Gets the length of the root of the path (drive, share, etc.).
        /// </summary>
        internal static int GetRootLength(string path) {
            int i = 0;
            int volumeSeparatorLength = 2; // Length to the colon "C:"
            int uncRootLength = 2; // Length to the start of the server name "\\"

            bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix);
            bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix);
            if (extendedSyntax) {
                // Shift the position we look for the root from to account for the extended prefix
                if (extendedUncSyntax) {
                    // "\\" -> "\\?\UNC\"
                    uncRootLength = UncExtendedPathPrefix.Length;
                }
                else {
                    // "C:" -> "\\?\C:"
                    volumeSeparatorLength += ExtendedDevicePathPrefix.Length;
                }
            }

            if ((!extendedSyntax || extendedUncSyntax) && path.Length > 0 && IsDirectorySeparator(path[0])) {
                // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")

                i = 1; //  Drive rooted (\foo) is one character
                if (extendedUncSyntax || (path.Length > 1 && IsDirectorySeparator(path[1]))) {
                    // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
                    // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
                    i = uncRootLength;
                    int n = 2; // Maximum separators to skip
                    while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
                }
            }
            else if (path.Length >= volumeSeparatorLength &&
                     path[volumeSeparatorLength - 1] == PathNetCore.VolumeSeparatorChar) {
                // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
                // If the colon is followed by a directory separator, move past it
                i = volumeSeparatorLength;
                if (path.Length >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
            }

            return i;
        }

        /// <summary>
        /// True if the given character is a directory separator.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsDirectorySeparator(char c) {
            return c == PathNetCore.DirectorySeparatorChar || c == PathNetCore.AltDirectorySeparatorChar;
        }

        /// <summary>
        /// Get the common path length from the start of the string.
        /// </summary>
        internal static int GetCommonPathLength(string first, string second, bool ignoreCase) {
            int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);

            // If nothing matches
            if (commonChars == 0)
                return commonChars;

            // Or we're a full string and equal length or match to a separator
            if (commonChars == first.Length
                && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
                return commonChars;

            if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
                return commonChars;

            // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
            while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
                commonChars--;

            return commonChars;
        }

        /// <summary>
        /// Gets the count of common characters from the left optionally ignoring case
        /// </summary>
        internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) {
            if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;

            int commonChars = 0;

            fixed (char* f = first)
            fixed (char* s = second) {
                char* l = f;
                char* r = s;
                char* leftEnd = l + first.Length;
                char* rightEnd = r + second.Length;

                while (l != leftEnd && r != rightEnd
                                    && (*l == *r || (ignoreCase &&
                                                     char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) {
                    commonChars++;
                    l++;
                    r++;
                }
            }

            return commonChars;
        }

        /// <summary>
        /// Returns true if the path ends in a directory separator.
        /// </summary>
        internal static bool EndsInDirectorySeparator(string path)
            => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
    }

    /// <summary> Tests for PathNetCore.GetRelativePath </summary>
    public static class GetRelativePathTests {
        [Theory]
        [InlineData(@"C:\", @"C:\", @".")]
        [InlineData(@"C:\a", @"C:\a\", @".")]
        [InlineData(@"C:\A", @"C:\a\", @".")]
        [InlineData(@"C:\a\", @"C:\a", @".")]
        [InlineData(@"C:\", @"C:\b", @"b")]
        [InlineData(@"C:\a", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\b\", @"..\b\")]
        [InlineData(@"C:\a\b", @"C:\a", @"..")]
        [InlineData(@"C:\a\b", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\a\b", @"b")]
        [InlineData(@"C:\a", @"C:\A\b", @"b")]
        [InlineData(@"C:\a", @"C:\b\c", @"..\b\c")]
        [InlineData(@"C:\a\", @"C:\a\b", @"b")]
        [InlineData(@"C:\", @"D:\", @"D:\")]
        [InlineData(@"C:\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\", @"D:\b\", @"D:\b\")]
        [InlineData(@"C:\a", @"D:\b", @"D:\b")]
        [InlineData(@"C:\a\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\ab", @"C:\a", @"..\a")]
        [InlineData(@"C:\a", @"C:\ab", @"..\ab")]
        [InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")]
        [InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")]
        //[PlatformSpecific(TestPlatforms.Windows)]  // Tests Windows-specific paths
        public static void GetRelativePath_Windows(string relativeTo, string path, string expected) {
            string result = PathNetCore.GetRelativePath(relativeTo, path);
            Assert.Equal(expected, result);

            // Check that we get the equivalent path when the result is combined with the sources
            Assert.Equal(
                Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar),
                Path.GetFullPath(Path.Combine(Path.GetFullPath(relativeTo), result))
                    .TrimEnd(Path.DirectorySeparatorChar),
                ignoreCase: true,
                ignoreLineEndingDifferences: false,
                ignoreWhiteSpaceDifferences: false);
        }
    }
}
Anton Krouglov
  • 3,077
  • 2
  • 29
  • 50
  • Wow, thank you so much! This works perfectly with .NET Framework 4.6.1. This answer should be upvoted or even accepted as solution. – j00hi Feb 09 '19 at 09:02
  • Thank you! With minor modification it also works in .NET Framework 4.5.2 – Bja Mar 04 '20 at 17:21
5

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.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
4

Use:

RelPath = AbsPath.Replace(ApplicationPath, ".")
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kevin
  • 13,044
  • 11
  • 55
  • 76
  • for a narrow set of cases this will work brilliantly! I going to use this! I think these other guys want a general purpose error-checking, edge-case-handling solution. but if this is all you need, it sure is simple! – DanO Apr 07 '13 at 18:08
  • I ended up using `path.Replace(rootPath.TrimEnd('\\') + "\\", "")`. – Konard Mar 01 '20 at 14:48
4

It's a long way around, but System.Uri class has a method named MakeRelativeUri. Maybe you could use that. It's a shame really that System.IO.Path doesn't have this.

Vilx-
  • 104,512
  • 87
  • 279
  • 422
3

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();
  }
}
Maxence
  • 12,868
  • 5
  • 57
  • 69
2

You want to use the CommonPath method of this RelativePath class. Once you have the common path, just strip it out of the path you want to display.

Namespace IO.Path

    Public NotInheritable Class RelativePath

        Private Declare Function PathRelativePathTo Lib "shlwapi" Alias "PathRelativePathToA" ( _
            ByVal pszPath As String, _
            ByVal pszFrom As String, _
            ByVal dwAttrFrom As Integer, _
            ByVal pszTo As String, _
            ByVal dwAttrTo As Integer) As Integer

        Private Declare Function PathCanonicalize Lib "shlwapi" Alias "PathCanonicalizeA" ( _
            ByVal pszBuf As String, _
            ByVal pszPath As String) As Integer

        Private Const FILE_ATTRIBUTE_DIRECTORY As Short = &H10S

        Private Const MAX_PATH As Short = 260

        Private _path As String
        Private _isDirectory As Boolean

#Region " Constructors "

        Public Sub New()

        End Sub

        Public Sub New(ByVal path As String)
            _path = path
        End Sub

        Public Sub New(ByVal path As String, ByVal isDirectory As Boolean)
            _path = path
            _isDirectory = isDirectory
        End Sub

#End Region

        Private Shared Function StripNulls(ByVal value As String) As String
            StripNulls = value
            If (InStr(value, vbNullChar) > 0) Then
                StripNulls = Left(value, InStr(value, vbNullChar) - 1)
            End If
        End Function

        Private Shared Function TrimCurrentDirectory(ByVal path As String) As String
            TrimCurrentDirectory = path
            If Len(path) >= 2 And Left(path, 2) = ".\" Then
                TrimCurrentDirectory = Mid(path, 3)
            End If
        End Function

        ''' <summary>
        ''' 3. conforming to general principles: conforming to accepted principles or standard practice
        ''' </summary>
        Public Shared Function Canonicalize(ByVal path As String) As String
            Dim sPath As String

            sPath = New String(Chr(0), MAX_PATH)

            If PathCanonicalize(sPath, path) = 0 Then
                Canonicalize = vbNullString
            Else
                Canonicalize = StripNulls(sPath)
            End If

        End Function

        ''' <summary>
        ''' Returns the most common path between two paths.
        ''' </summary>
        ''' <remarks>
        ''' <para>returns the path that is common between two paths</para>
        ''' <para>c:\FolderA\FolderB\FolderC</para>
        '''   c:\FolderA\FolderD\FolderE\File.Ext
        ''' 
        '''   results in:
        '''       c:\FolderA\
        ''' </remarks>
        Public Shared Function CommonPath(ByVal path1 As String, ByVal path2 As String) As String
            'returns the path that is common between two paths
            '
            '   c:\FolderA\FolderB\FolderC
            '   c:\FolderA\FolderD\FolderE\File.Ext
            '
            '   results in:
            '       c:\FolderA\

            Dim sResult As String = String.Empty
            Dim iPos1, iPos2 As Integer
            path1 = Canonicalize(path1)
            path2 = Canonicalize(path2)
            Do
                If Left(path1, iPos1) = Left(path2, iPos2) Then
                    sResult = Left(path1, iPos1)
                End If
                iPos1 = InStr(iPos1 + 1, path1, "\")
                iPos2 = InStr(iPos2 + 1, path1, "\")
            Loop While Left(path1, iPos1) = Left(path2, iPos2)

            Return sResult

        End Function

        Public Function CommonPath(ByVal path As String) As String
            Return CommonPath(_path, path)
        End Function

        Public Shared Function RelativePathTo(ByVal source As String, ByVal isSourceDirectory As Boolean, ByVal target As String, ByVal isTargetDirectory As Boolean) As String
            'DEVLIB
            '   05/23/05  1:47PM - Fixed call to PathRelativePathTo, iTargetAttribute is now passed to dwAttrTo instead of IsTargetDirectory.
            '       For Visual Basic 6.0, the fix does not change testing results,
            '           because when the Boolean IsTargetDirectory is converted to the Long dwAttrTo it happens to contain FILE_ATTRIBUTE_DIRECTORY,
            '
            Dim sRelativePath As String
            Dim iSourceAttribute, iTargetAttribute As Integer

            sRelativePath = New String(Chr(0), MAX_PATH)
            source = Canonicalize(source)
            target = Canonicalize(target)

            If isSourceDirectory Then
                iSourceAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If isTargetDirectory Then
                iTargetAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If PathRelativePathTo(sRelativePath, source, iSourceAttribute, target, iTargetAttribute) = 0 Then
                RelativePathTo = vbNullString
            Else
                RelativePathTo = TrimCurrentDirectory(StripNulls(sRelativePath))
            End If

        End Function

        Public Function RelativePath(ByVal target As String) As String
            Return RelativePathTo(_path, _isDirectory, target, False)
        End Function

    End Class

End Namespace
Shog9
  • 156,901
  • 35
  • 231
  • 235
AMissico
  • 21,470
  • 7
  • 78
  • 106
2

If you're sure that your absolute path 2 is always relative to absolute path, just remove the first N characters from path2, where N is the length of path1.

1

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;
    }
Szybki
  • 1,083
  • 14
  • 29
1

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.

3Doubloons
  • 2,088
  • 14
  • 26
  • Totally agree - there are many instances where the relative path could be the full pathname e.g. your common root was drive - c:\ - so you'd still have to handle this case. – stephbu Nov 09 '08 at 08:00
1

If you have a readonly text box, could you not not make it a label and set AutoEllipsis=true?

alternatively there are posts with code for generating the autoellipsis yourself: (this does it for a grid, you would need to pass i the width for the text box instead. It isn't quite right as it hacks off a bit more than is necessary, and I haven;t got around to finding where the calculation is incorrect. it would be easy enough to modify to remove the first part of the directory rather than the last if you desire.

Private Function AddEllipsisPath(ByVal text As String, ByVal colIndex As Integer, ByVal grid As DataGridView) As String
    'Get the size with the column's width 
    Dim colWidth As Integer = grid.Columns(colIndex).Width

    'Calculate the dimensions of the text with the current font
    Dim textSize As SizeF = MeasureString(text, grid.Font)

    Dim rawText As String = text
    Dim FileNameLen As Integer = text.Length - text.LastIndexOf("\")
    Dim ReplaceWith As String = "\..."

    Do While textSize.Width > colWidth
        ' Trim to make room for the ellipsis
        Dim LastFolder As Integer = rawText.LastIndexOf("\", rawText.Length - FileNameLen - 1)

        If LastFolder < 0 Then
            Exit Do
        End If

        rawText = rawText.Substring(0, LastFolder) + ReplaceWith + rawText.Substring(rawText.Length - FileNameLen)

        If ReplaceWith.Length > 0 Then
            FileNameLen += 4
            ReplaceWith = ""
        End If
        textSize = MeasureString(rawText, grid.Font)
    Loop

    Return rawText
End Function

Private Function MeasureString(ByVal text As String, ByVal fontInfo As Font) As SizeF
    Dim size As SizeF
    Dim emSize As Single = fontInfo.Size
    If emSize = 0 Then emSize = 12

    Dim stringFont As New Font(fontInfo.Name, emSize)

    Dim bmp As New Bitmap(1000, 100)
    Dim g As Graphics = Graphics.FromImage(bmp)

    size = g.MeasureString(text, stringFont)
    g.Dispose()
    Return size
End Function
Shog9
  • 156,901
  • 35
  • 231
  • 235
CestLaGalere
  • 2,909
  • 1
  • 20
  • 20
1

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);
}
Cameron Stone
  • 857
  • 1
  • 8
  • 10
  • 2
    What if they're in different folders? This doesn't append "..". What if one of the paths contains ".." already? This would return the wrong level of relative path. What if file A is in "MyFolder" and file B is in "MyLunchbox" -- this method doesn't know about directory separator chars, so it would just think "Lunchbox\File" was the correct path. This is awful. – BrainSlugs83 Nov 08 '13 at 20:57
1

Adapted answer from duplicate: How to make an absolute path relative to a particular folder? and updated to match parameter sequence as the .Net standard implementation

This is a more concise implementation for .Net Framework that has thorough guidance in the doc comments and accounts for absolute paths that have relative folder links which is missing from many other solutions.

Tested against Anton Krouglov's test cases in .Net 6 all cases match System.IO.Path.GetRelativePath :)

public static partial class PathUtilities
{
    /// <summary>
    /// Rebases file with <paramref name="path"/> to the folder specified by <paramref name="relativeTo"/>.
    /// </summary>
    /// <param name="path">Full file path (absolute)</param>
    /// <param name="relativeTo">Full base directory path (absolute) the result should be relative to. This path is always considered to be a directory.</param>
    /// <returns>Relative path to file with respect to <paramref name="relativeTo"/></returns>
    /// <remarks>Paths are resolved by calling the <seealso cref="System.IO.Path.GetFullPath(string)"/> method before calculating the difference. This will resolve 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 GetRelativePath(string relativeTo, string path)
    {
        String pathSep = "\\";
        String itemPath = Path.GetFullPath(path);
        String baseDirPath = Path.GetFullPath(relativeTo); // If folder contains upper folder references, they get resolved here. "c:\test\..\test2" => "c:\test2"
        bool isDirectory = path.EndsWith(pathSep);

        String[] p1 = Regex.Split(itemPath, "[\\\\/]").Where(x => x.Length != 0).ToArray();
        String[] p2 = Regex.Split(relativeTo, "[\\\\/]").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)));
        if (String.IsNullOrEmpty(r)) return ".";
        else if (isDirectory && p1.Length >= p2.Length) // only append on forward traversal, to match .Net Standard Implementation of System.IO.Path.GetRelativePath
            r += pathSep;

        return r;
    }
}

Usage of this method:

string itemPath = @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat";
string baseDirectory = @"C:\Program Files\Dummy Folder\MyProgram";
string result = PathUtilities.GetRelativePath(baseDirectory, itemPath);
Console.WriteLine(result);

Results in:

Data\datafile1.dat

RE: But if the file is at the root directory or 1 directory below root, then display the full path.
In this case we could modify the method, or simply perform an additional check:

 string itemPath = @"C:\Program Files\Dummy Folder\datafile1.dat";
 string baseDirectory = @"C:\Program Files\Dummy Folder\MyProgram";
 string result = PathUtilities.GetRelativePath(baseDirectory, itemPath);
 Console.WriteLine("Before Check: '{0}'", result);
 if (result.StartsWith("..\\"))
     result = itemPath;
 Console.WriteLine("After Check: '{0}'", result);

Result:

 Before Check: '..\datafile1.dat'
 After Check: 'C:\Program Files\Dummy Folder\datafile1.dat'
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
0
    public static string ToRelativePath(string filePath, string refPath)
    {
        var pathNormalized = Path.GetFullPath(filePath);

        var refNormalized = Path.GetFullPath(refPath);
        refNormalized = refNormalized.TrimEnd('\\', '/');

        if (!pathNormalized.StartsWith(refNormalized))
            throw new ArgumentException();
        var res = pathNormalized.Substring(refNormalized.Length + 1);
        return res;
    }
user626528
  • 13,999
  • 30
  • 78
  • 146
0

This should work:

private string rel(string path) {
  string[] cwd  = new Regex(@"[\\]").Split(Directory.GetCurrentDirectory());
  string[] fp   = new Regex(@"[\\]").Split(path);

  int common = 0;

  for (int n = 0; n < fp.Length; n++) {
    if (n < cwd.Length && n < fp.Length && cwd[n] == fp[n]) {
      common++;
    }
  }

  if (common > 0) {
    List<string> rp = new List<string>();

    for (int n = 0; n < (cwd.Length - common); n++) {
      rp.Add("..");
    }

    for (int n = common; n < fp.Length; n++) {
      rp.Add(fp[n]);
    }

    return String.Join("/", rp.ToArray());
  } else {
    return String.Join("/", fp);
  }
}
excanoe
  • 696
  • 9
  • 16
0

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.

0

here's mine:

public static string RelativePathTo(this System.IO.DirectoryInfo @this, string to)
{
    var rgFrom = @this.FullName.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var rgTo = to.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var cSame = rgFrom.TakeWhile((p, i) => i < rgTo.Length && string.Equals(p, rgTo[i])).Count();

    return Path.Combine(
        Enumerable.Range(0, rgFrom.Length - cSame)
        .Select(_ => "..")
        .Concat(rgTo.Skip(cSame))
        .ToArray()
    );
}
Spongman
  • 9,665
  • 8
  • 39
  • 58
0

Play with something like:

private String GetRelativePath(Int32 level, String directory, out String errorMessage) {
        if (level < 0 || level > 5) {
            errorMessage = "Find some more smart input data";
            return String.Empty;
        }
        // ==========================
        while (level != 0) {
            directory = Path.GetDirectoryName(directory);
            level -= 1;
        }
        // ==========================
        errorMessage = String.Empty;
        return directory;
    }

And test it

[Test]
    public void RelativeDirectoryPathTest() {
        var relativePath =
            GetRelativePath(3, AppDomain.CurrentDomain.BaseDirectory, out var errorMessage);
        Console.WriteLine(relativePath);
        if (String.IsNullOrEmpty(errorMessage) == false) {
            Console.WriteLine(errorMessage);
            Assert.Fail("Can not find relative path");
        }
    }
Sergey Orlov
  • 491
  • 5
  • 16
0

In ASP.NET Core 2, if you want the relative path to bin\Debug\netcoreapp2.2 you can use the following combination:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
public class RenderingService : IRenderingService
{

    private readonly IHostingEnvironment _hostingEnvironment;
    public RenderingService(IHostingEnvironment hostingEnvironment)
    {
    _hostingEnvironment = hostingEnvironment;
    }

    public string RelativeAssemblyDirectory()
    {
        var contentRootPath = _hostingEnvironment.ContentRootPath;
        string executingAssemblyDirectoryAbsolutePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        string executingAssemblyDirectoryRelativePath = System.IO.Path.GetRelativePath(contentRootPath, executingAssemblyDirectoryAbsolutePath);
        return executingAssemblyDirectoryRelativePath;
    }
}
Dragos Durlut
  • 8,018
  • 10
  • 47
  • 62