如何在Windows上获取区分大小写的路径?

41

我需要知道给定路径的真实路径。

例如:

真实路径是:d:\src\File.txt
用户提供的是:D:\src\file.txt
我需要的结果是:d:\src\File.txt


4
我认为Windows的文件系统基本上是不区分大小写的。既然如此,这种做法最多只是多余的,而且最坏情况下可能是无意义的。 :) - Dan J
13
@djacobson:你错了。Windows本质上是大小写敏感的,但某些标志使其表现为不区分大小写。搜索“OBJ_CASE_INSENSITIVE”以获取详细信息。例如,如果你正在编写一个BASH模拟器,可能需要使用区分大小写的路径,这样你自然需要正确的文件大小写。 - user541686
5
我需要应用已经做出的更改到一个区分大小写的平台上,因此我需要知道在另一侧查找的真实路径。 - Rodrigo
1
@Rodrigo:是的,你的问题完全有效。我会发布一个更长(但更健壮)的解决方案,适用于所有情况。 - user541686
3
@Mehrdad Ah,进行了一些研究证明您是正确的。我承认错误! - Dan J
显示剩余2条评论
9个回答

19
你可以使用这个函数:
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
static extern uint GetLongPathName(string ShortPath, StringBuilder sb, int buffer);

[DllImport("kernel32.dll")]
static extern uint GetShortPathName(string longpath, StringBuilder sb, int buffer); 

protected static string GetWindowsPhysicalPath(string path)
{
        StringBuilder builder = new StringBuilder(255);

        // names with long extension can cause the short name to be actually larger than
        // the long name.
        GetShortPathName(path, builder, builder.Capacity);

        path = builder.ToString();

        uint result = GetLongPathName(path, builder, builder.Capacity);

        if (result > 0 && result < builder.Capacity)
        {
            //Success retrieved long file name
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        if (result > 0)
        {
            //Need more capacity in the buffer
            //specified in the result variable
            builder = new StringBuilder((int)result);
            result = GetLongPathName(path, builder, builder.Capacity);
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        return null;
}

1
你在禁用短文件名的文件系统上测试过这个吗? - Harry Johnston
3
我测试了一下,发现GetShortPathName函数并没有起作用,没有错误被抛出或返回,但如果指定了长路径名,它只会返回长路径名。 - Christoph
对我来说也完美运行。但是,驱动器字母始终为小写。例如:在Windows上路径为:D:\Test\test.txt,该函数返回d:\Test\test.txt - A-Sharabiani
只有在文件实际上设置了其短名称时,此方法才有效。正如您可以在文档中看到的那样,这并非强制要求。 - AI0867
2
这种方法并不适用于所有情况。仍然有些问题。 - ceztko
显示剩余5条评论

10

作为一名老手,我通常使用FindFirstFile来实现这个目的。.Net的翻译如下:

Directory.GetFiles(Path.GetDirectoryName(userSuppliedName), Path.GetFileName(userSuppliedName)).FirstOrDefault();

这只能让你得到路径中文件名部分的正确大小写,而不是整个路径。

JeffreyLWhitledge的评论提供了一个递归版本的链接,可以(虽然不总是)解析完整路径。


不错;喜欢那个没有dllimports的一行代码。 - milkplus
1
这不会生成所需的正确输出路径。 - Paul
@Paul,你能给一个具体的例子说明这个失败了吗? - Tergiver
3
你实际尝试过这个吗?我完全无法使用。目录大小写仍然来自用户提供的名称。尝试了几个 .NET 版本,结果都一样。 - Jeffrey L Whitledge
@JeffreyLWhitledge 看看这个回答:https://dev59.com/83RB5IYBdhLWcg3w4bGv#479198 - kay.one

5

备用解决方案

这是一个适用于我在Windows和服务器之间使用区分大小写的路径移动文件的解决方案。它会遍历整个目录树,并使用GetFileSystemEntries()来校正每个条目。如果路径的一部分无效(UNC或文件夹名称),则仅纠正该部分路径,然后对于找不到的内容使用原始路径。希望这能为其他人在处理相同问题时节省时间。

private string GetCaseSensitivePath(string path)
{
    var root = Path.GetPathRoot(path);
    try
    {
        foreach (var name in path.Substring(root.Length).Split(Path.DirectorySeparatorChar))
            root = Directory.GetFileSystemEntries(root, name).First();
    }
    catch (Exception e)
    {
        // Log("Path not found: " + path);
        root += path.Substring(root.Length);
    }
    return root;
}

1
该方法不检查文件名大小写。这并不是实际问题的有效答案。 - Dodger
2
运行良好,即使文件名大小写不同!+1 我建议使用EnumerateFileSystemEntries而不是GetFileSystemEntries。 - maliger

3
获取文件的实际路径(对于文件夹不适用)的步骤如下:
1. 调用 CreateFileMapping 创建一个文件映射。
2. 调用 GetMappedFileName 获取文件名。
3. 使用 QueryDosDevice 将文件名转换为 MS-DOS 风格的路径名。
如果您想编写一个更健壮的程序,也可以处理目录(但需要更多的痛苦和一些未记录的特性),请按照以下步骤操作:
1. 使用 CreateFileNtOpenFile 获取文件/文件夹句柄。
2. 调用 NtQueryObject 获取完整路径名。
3. 使用 NtQueryInformationFileFileNameInformation 获取卷相对路径。
4. 利用上述两个路径,获取代表卷本身的路径组件。例如,如果第一个路径为 \Device\HarddiskVolume1\Hello.txt,第二个路径为 \Hello.txt,则现在您知道卷的路径为 \Device\HarddiskVolume1
5. 使用文档不全的 Mount Manager I/O 控制码或者 QueryDosDevice,将完整的 NT 风格路径中的卷部分替换为驱动器号。
现在您已经拥有了文件的真实路径。

假设你有一个目录,你可以创建一个临时文件,使用第一种技术获取文件的实际路径,然后去掉文件名部分?(当然,如果你有写入权限的话。) - Harry Johnston
1
自Windows Vista开始,也有GetFinalPathNameByHandle函数。 - Harry Johnston

3
这里有一种备选解决方案,适用于文件和目录。使用GetFinalPathNameByHandle进行翻译,根据文档,仅受支持的Vista / Server2008或以上桌面应用程序。
请注意,如果您提供符号链接,则会解析它,这是找到“最终”路径的一部分。
// http://www.pinvoke.net/default.aspx/shell32/GetFinalPathNameByHandle.html
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint GetFinalPathNameByHandle(SafeFileHandle hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
private const uint FILE_NAME_NORMALIZED = 0x0;

static string GetFinalPathNameByHandle(SafeFileHandle fileHandle)
{
    StringBuilder outPath = new StringBuilder(1024);

    var size = GetFinalPathNameByHandle(fileHandle, outPath, (uint)outPath.Capacity, FILE_NAME_NORMALIZED);
    if (size == 0 || size > outPath.Capacity)
        throw new Win32Exception(Marshal.GetLastWin32Error());

    // may be prefixed with \\?\, which we don't want
    if (outPath[0] == '\\' && outPath[1] == '\\' && outPath[2] == '?' && outPath[3] == '\\')
        return outPath.ToString(4, outPath.Length - 4);

    return outPath.ToString();
}

// http://www.pinvoke.net/default.aspx/kernel32.createfile
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFileHandle CreateFile(
     [MarshalAs(UnmanagedType.LPTStr)] string filename,
     [MarshalAs(UnmanagedType.U4)] FileAccess access,
     [MarshalAs(UnmanagedType.U4)] FileShare share,
     IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
     [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
     [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
     IntPtr templateFile);
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

public static string GetFinalPathName(string dirtyPath)
{
    // use 0 for access so we can avoid error on our metadata-only query (see dwDesiredAccess docs on CreateFile)
    // use FILE_FLAG_BACKUP_SEMANTICS for attributes so we can operate on directories (see Directories in remarks section for CreateFile docs)

    using (var directoryHandle = CreateFile(
        dirtyPath, 0, FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, FileMode.Open,
        (FileAttributes)FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero))
    {
        if (directoryHandle.IsInvalid)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        return GetFinalPathNameByHandle(directoryHandle);
    }
}

我尝试了5种其他的解决方案,这是第一个适用于D:和C:驱动器路径的。谢谢! - Jannik

2
由于Borja的答案无法在禁用8.3名称的卷上工作,因此这里是Tergiver建议的递归实现(适用于文件和文件夹,以及UNC共享的文件和文件夹,但不适用于它们的计算机名称或共享名称)。
不存在的文件或文件夹没有问题,已验证并进行了更正,但您可能会遇到文件夹重定向问题,例如尝试获取“C:\WinDoWs\sYsteM32\driVErs\eTC\Hosts”的正确路径时,您将在64位Windows上获得“C:\Windows\System32\drivers\eTC\hosts”,因为在“C:\Windows\sysWOW64\drivers”中没有“etc”文件夹。
测试场景:
        Directory.CreateDirectory(@"C:\Temp\SomeFolder");
        File.WriteAllLines(@"C:\Temp\SomeFolder\MyTextFile.txt", new String[] { "Line1", "Line2" });

使用方法:

        FileInfo myInfo = new FileInfo(@"C:\TEMP\SOMEfolder\MyTeXtFiLe.TxT");
        String myResult = myInfo.GetFullNameWithCorrectCase(); //Returns "C:\Temp\SomeFolder\MyTextFile.txt"

代码:

public static class FileSystemInfoExt {

    public static String GetFullNameWithCorrectCase(this FileSystemInfo fileOrFolder) {
        //Check whether null to simulate instance method behavior
        if (Object.ReferenceEquals(fileOrFolder, null)) throw new NullReferenceException();
        //Initialize common variables
        String myResult = GetCorrectCaseOfParentFolder(fileOrFolder.FullName);
        return myResult;
    }

    private static String GetCorrectCaseOfParentFolder(String fileOrFolder) {
        String myParentFolder = Path.GetDirectoryName(fileOrFolder);
        String myChildName = Path.GetFileName(fileOrFolder);
        if (Object.ReferenceEquals(myParentFolder, null)) return fileOrFolder.TrimEnd(new char[]{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
        if (Directory.Exists(myParentFolder)) {
            //myParentFolder = GetLongPathName.Invoke(myFullName);
            String myFileOrFolder = Directory.GetFileSystemEntries(myParentFolder, myChildName).FirstOrDefault();
            if (!Object.ReferenceEquals(myFileOrFolder, null)) {
                myChildName = Path.GetFileName(myFileOrFolder);
            }
        }
        return GetCorrectCaseOfParentFolder(myParentFolder) + Path.DirectorySeparatorChar + myChildName;
    }

}

0

这是我如何做到的。最初,我依赖于GetFinalPathNameByHandle,它非常好用,但不幸的是,一些自定义文件系统不支持它(当然NTFS支持)。我还尝试了NtQueryObjectObjectNameInformation,但同样地,它们不一定报告原始文件名。

所以这里有另一种“手动”的方法:

public static string GetRealPath(string fullPath)
{
    if (fullPath == null)
        return null; // invalid

    var pos = fullPath.LastIndexOf(Path.DirectorySeparatorChar);
    if (pos < 0 || pos == (fullPath.Length - 1))
        return fullPath.ToUpperInvariant(); // drive letter

    var dirPath = fullPath.Substring(0, pos);
    var realPath = GetRealPath(dirPath); // go recursive, we want the final full path
    if (realPath == null)
        return null; // doesn't exist

    var dir = new DirectoryInfo(realPath);
    if (!dir.Exists)
        return null; // doesn't exist
    
    var fileName = fullPath.Substring(pos + 1);
    if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) // avoid wildcard calls
        return null;

    return dir.EnumerateFileSystemInfos(fileName).FirstOrDefault()?.FullName; // may return null
}

0

我尝试避免使用dll导入,所以对我来说最好的方法是使用System.Linq和System.IO.Directory类。

对于你的例子 真实路径是:d:\src\File.txt 用户给我的是:D:\src\file.txt

代码如下:

using System.Linq;

public static class PathUtils
{
    public static string RealPath(string inputPath)
    {
        return Directory.GetFiles(Path.GetDirectoryName(inputPath))
            .FirstOrDefault(p => String.Equals(Path.GetFileName(p), 
                Path.GetFileName(inputPath), StringComparison.OrdinalIgnoreCase));
    }
}

var p = PathUtils.RealPath(@"D:\src\file.txt");

该方法应返回路径 "d:\src\File.txt" 或 "D:\src\File.txt"。


这仅适用于不区分大小写的操作系统,因为在区分大小写的操作系统中,GetDirectoryName会抛出异常。 - Dodger

-4
在Windows上,路径是不区分大小写的。因此,这两个路径都是同样真实的。
如果您想获取某种具有规范大写形式的路径(即Windows认为应该大写的方式),则可以使用路径作为掩码调用FindFirstFile(),然后取找到的文件的全名。如果路径无效,则自然不会得到规范名称。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接