18

Here's the problem, I have a bunch of directories like

S:\HELLO\HI
S:\HELLO2\HI\HElloAgain

On the file system it shows these directories as

S:\hello\Hi
S:\hello2\Hi\helloAgain

Is there any function in C# that will give me what the file system name of a directory is with the proper casing?

Pavel Vladov
  • 4,707
  • 3
  • 35
  • 39
Tom
  • 1,270
  • 2
  • 12
  • 25
  • There's no function to do that, and trying to normalize strings like "HElloAgain" to "helloAgain" will be a problem as even Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase("HElloAgain") would return "Helloagain. – Agent_9191 Jul 31 '09 at 20:30
  • @Agent, DirectoryInfo.GetDirectories() returns folders in the file system's case. – Kevin Jul 31 '09 at 21:08
  • 1
    Accepted, wow I had no idea new answers were here. Thanks to both Iceman and Bertu for helping solve this. – Tom Mar 21 '11 at 20:56

4 Answers4

9

string FileSystemCasing = new System.IO.DirectoryInfo("H:\...").FullName;

EDIT:

As iceman pointed out, the FullName returns the correct casing only if the DirectoryInfo (or in general the FileSystemInfo) comes from a call to the GetDirectories (or GetFileSystemInfos) method.

Now I'm posting a tested and performance-optimized solution. It work well both on directory and file paths, and has some fault tolerance on input string. It's optimized for "conversion" of single paths (not the entire file system), and faster than getting the entire file system tree. Of course, if you have to renormalize the entire file system tree, you may prefer iceman's solution, but i tested on 10000 iterations on paths with medium level of deepness, and it takes just few seconds ;)

    private string GetFileSystemCasing(string path)
    {
        if (Path.IsPathRooted(path))
        {
            path = path.TrimEnd(Path.DirectorySeparatorChar); // if you type c:\foo\ instead of c:\foo
            try
            {
                string name = Path.GetFileName(path);
                if (name == "") return path.ToUpper() + Path.DirectorySeparatorChar; // root reached

                string parent = Path.GetDirectoryName(path); // retrieving parent of element to be corrected

                parent = GetFileSystemCasing(parent); //to get correct casing on the entire string, and not only on the last element

                DirectoryInfo diParent = new DirectoryInfo(parent);
                FileSystemInfo[] fsiChildren = diParent.GetFileSystemInfos(name);
                FileSystemInfo fsiChild = fsiChildren.First();
                return fsiChild.FullName; // coming from GetFileSystemImfos() this has the correct case
            }
            catch (Exception ex) { Trace.TraceError(ex.Message); throw new ArgumentException("Invalid path"); }
            return "";
        }
        else throw new ArgumentException("Absolute path needed, not relative");
    }
BertuPG
  • 653
  • 4
  • 6
  • 1
    This doesn't actually work. It returns the same case as you pass into the DirectoryInfo constructor and ignores the actual case stored in the filesystem, but it's the filesystem case that he wants. – Kevin Feb 24 '11 at 17:11
  • You are right. I edited to submit a complete working example. – BertuPG Feb 25 '11 at 10:30
  • 2
    I apologize. Your first comment seemed more hostile than you probably meant, and I should have let it go instead of starting an argument. I should really stay off the Internet when I'm grumpy. Splitting the path and getting the case of each piece is a really good idea, and it makes the difference between painfully slow and usable. I can't upvote your answer unless you edit it, but that didn't stop me from upvoting some of your other answers. Sorry for being a jerk--I owe you a drink if you're ever in Denver. – Kevin Mar 02 '11 at 22:37
  • This was really handy for "normalising" a .. path: `_dataDir = new DirectoryInfo(Application.StartupPath + @"\..\..\Data\").FullName;` – corlettk Dec 30 '11 at 10:19
4

Here's a basic and relatively fast solution, keep reading below for some commentary:

private static string GetCase(string path)
{      
  DirectoryInfo dir = new DirectoryInfo(path);
  if (dir.Exists)
  {
    string[] folders = dir.FullName.Split(Path.DirectorySeparatorChar);
    dir = dir.Root;

    foreach (var f in folders.Skip(1))
    {          
      dir = dir.GetDirectories(f).First();
    }

    return dir.FullName;
  }
  else
  {
    return path;
  }
}

The basic idea is that getting subdirectories from a DirectoryInfo object will get you the correct case, so we just need to split the directory name and walk from the root to the target directory, getting the proper case at each step.

My initial answer relied on getting the casing for every folder on the drive, and it worked but was slow. I came up with a slight improvement that stored the results, but it was still too slow for everyday usage. You can see the edit history for this comment if you need to do this for every thing on the drive, and even then there are probably ways to speed up that code. It was "here's how you might do it" and not "here's a great way to do it."

Bertu, in his answer, came up with the idea of splitting the path into its components and getting the casing piece by piece, which results in a huge speed increase since you're no longer checking everything as in my original answer. Bertu also generalized his solution to do files as well as directories. In my tests, the code posted above (which uses Bertu's "split the path and do it by pieces" idea but approaches it iteratively instead of recursively) runs in about half the time of Bertu's code. I'm not sure if that's because his method also handles files, because his use of recursion introduces extra overhead, or because he ends up calling Path.GetFileName(path) and Path.GetDirectoryName(path) in each iteration. Depending on your exact needs, some combination of his answer and mine will likely solve your problem as well as is possible in C#.

On that note, I should mention that there are some limitations to .Net filename handling, and since doing this in .Net requires making a lot of DirectoryInfo objects, you might want to consider unmanaged code if this is your bottleneck.

Kevin
  • 8,353
  • 3
  • 37
  • 33
  • this works but it is PAINFULLY slow since it does a full system scan for search directory and I have over 100 directories to normalize – Tom Aug 20 '09 at 16:19
  • @Tom, see my update. I should have thought about multiple directories initially. – Kevin Aug 20 '09 at 23:10
  • 1) you downvoted my post before 2) I found this question with no accepted answers, an made one based on MY OWN knowledge. 3) Reading the code, you can find that my approach is TOTALLY DIFFERENT from yours: you get the entire file system tree in correct case, and do a case-insensitive search, while I start from the full path and climb the hierarchy to get each parents' correct name. If I used the FullName property of FileSystemInfo objects generated by GetFileSystemInfo method,is only beacuse it's the SOLE built-in method in .net that can do the work,and you DON'T have copyrights on it – BertuPG Mar 02 '11 at 08:48
  • You just now removed the downvote (as I now do also). As you can read in comments and in edit, I never hidden your contribute, but i don't take ANY part from your code. And event if there are some pieces in common, the key point (and the real added value) of my solution is the use of `GetDirectoryName` to get the parent directory. And, by the way, you just now rewrote your entire post to make it simpler and faster, making the same thing you're accusing me for. Only NOW that your solution is very cheap and fast, I not just remove the downvote, but also upvote. – BertuPG Mar 02 '11 at 17:06
  • I would like to note that this solution is incomplete because, when given `c:\windows`, it outputs `c:\Windows` instead of `C:\Windows`. @BertuPG’s new solution gets that right at least ;-). – binki Jan 18 '17 at 22:33
  • Wow, I looked back at your prior revisions, and I’m not sure if scanning the whole filesystem tree into memory would even run on my system without running out of memory. I should try it… oh, it failed rather quickly with `Unhandled Exception: System.UnauthorizedAccessException: Access to the path 'c:\$Recycle.Bin\S-1-5-18' is denied.` – binki Jan 18 '17 at 22:50
  • @binki, ignoring the access denied issue, I'd be surprised if the full filesystem tree didn't fit in RAM. DirectoryInfo objects are fairly small, so you'd need a bunch of files and very little memory for that to happen. Also I didn't fix the drive letter case because that wasn't part of the question. :) – Kevin Jan 19 '17 at 23:09
  • @Kevin Well, I can’t test that because even if I run elevated I get `Unhandled Exception: System.UnauthorizedAccessException: Access to the path 'c:\Documents and Settings' is denied.`. I guess you could claim that normalizing the full path isn’t part of the question just because the question never said that `s:\…` might be an input, but… what’s the point of normalizing any of the path if not the whole thing? – binki Jan 19 '17 at 23:17
  • @binki, not sure what to tell you, the question asked for getting the correct case of directories and the original code worked but was slow. Don't have the time or motivation to sort out permission issues for you on six year old code that I've already replaced with a better version, sorry. – Kevin Jan 19 '17 at 23:46
  • Well, for all it’s worth, I consider the root directory part of the directory name. Maybe that’s just a non-Windows-user attitude or something, heh. – binki Jan 20 '17 at 03:41
  • @binki, Then you're welcome to use Bertu's code which handled that but ran slower, or, as as I very clearly said in my answer, some combination of both. Really not sure why this warranted a downvote but whatever. – Kevin Jan 20 '17 at 17:38
0

My version (similar to BertuPG's and Kevin's using GetDirectories() and GetFileSystemInfos() but allowing for non-existent paths and files and working as an extension method):

/// <summary>
/// Gets the full path with all the capitalization properly done as it exists in the file system.
/// Non-existent paths returned as passed in.
/// </summary>
/// <param name="path">
/// The path to make full and make properly cased.
/// </param>
/// <returns>
/// The complete and capitalization-accurate path based on the 
/// given <paramref name="path"/>.
/// </returns>
/// <remarks>
/// Walks the path using <see cref="DirectoryInfo"/>.<see cref="DirectoryInfo.GetDirectories()"/>
/// and <see cref="DirectoryInfo.GetFileSystemInfos()"/> so don't expect awesome performance.
/// </remarks>
public static string GetFileSystemFullPath(this string path)
{
    if (path == null)
    {
        return path;
    }

    string[] pathParts = Path.GetFullPath(path).Split(Path.DirectorySeparatorChar);
    if (pathParts.Any())
    {
        var dir = new DirectoryInfo(pathParts[0]).Root;

        for (int i = 1; i < pathParts.Length; ++i)
        {
            var next = dir.GetDirectories(pathParts[i]).FirstOrDefault();
            if (next == null)
            {
                if (i == pathParts.Length - 1)
                {
                    var fileInfo = dir.GetFileSystemInfos(pathParts[i]).FirstOrDefault();
                    if (fileInfo != null)
                    {
                        return fileInfo.FullName;
                    }
                }

                var ret = dir.FullName;
                do
                {
                    ret = Path.Combine(ret, pathParts[i++]);
                } while (i < pathParts.Length);

                return ret;
            }

            dir = next;
        }

        return dir.FullName;
    }

    // cannot do anything with it.
    return path;
}
mheyman
  • 4,211
  • 37
  • 34
-3

      static void Main( string[] args )
      {
         string[] paths = new string[] { "S:\hello\Hi", "S:\hello2\Hi\helloAgain" };
         foreach( string aPath in paths )
         {
            string normalizedPath = NormalizePath( aPath );
            Console.WriteLine( "Previous: '{0}', Normalized: '{1}'", aPath, normalizedPath );
         }
         Console.Write( "\n\n\nPress any key..." );
         Console.Read();
      }

  public static string NormalizePath( string path )
  {
     StringBuilder sb = new StringBuilder( path );
     string[] paths = path.Split('\\');
     foreach( string folderName in paths )
     {
        string normalizedFolderName = ToProperCase( folderName );
        sb.Replace( folderName, normalizedFolderName );
     }
     return sb.ToString();
  }

  /// <summary>
  /// Converts a string to first character upper and rest lower (Camel Case).
  /// </summary>
  /// <param name="stringValue"></param>
  /// <returns></returns>
  public static string ToProperCase( string stringValue )
  {
     if( string.IsNullOrEmpty( stringValue ) )
        return stringValue;

     return CultureInfo.CurrentCulture.TextInfo.ToTitleCase( stringValue.ToLower() );
  }

  • 2
    He's asking for the casing as stored on the file system, not necessarily TitleCasing. – Kevin Jul 31 '09 at 21:10