在 Windows 上获取实际文件名(带有正确的大小写)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/74451/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-15 11:12:33  来源:igfitidea点击:

Getting actual file name (with proper casing) on Windows

c++cwindowswinapi

提问by NeARAZ

Windows file system is case insensitive. How, given a file/folder name (e.g. "somefile"), I get the actualname of that file/folder (e.g. it should return "SomeFile" if Explorer displays it so)?

Windows 文件系统不区分大小写。如何,给定文件/文件夹名称(例如“somefile”),我如何获得该文件/文件夹的实际名称(例如,如果资源管理器显示它,它应该返回“SomeFile”)?

Some ways I know, all of which seem quite backwards:

我知道的一些方法,所有这些似乎都很倒退:

  1. Given the full path, search for each folder on the path (via FindFirstFile). This gives proper cased results of each folder. At the last step, search for the file itself.
  2. Get filename from handle (as in MSDN example). This requires opening a file, creating file mapping, getting it's name, parsing device names etc. Pretty convoluted. And it does not work for folders or zero-size files.
  1. 给定完整路径,搜索路径上的每个文件夹(通过 FindFirstFile)。这给出了每个文件夹的正确大小写结果。在最后一步,搜索文件本身。
  2. 从句柄中获取文件名(如MSDN 示例中所示)。这需要打开一个文件、创建文件映射、获取它的名称、解析设备名称等。相当复杂。它不适用于文件夹或零大小的文件。

Am I missing some obvious WinAPI call? The simplest ones, like GetActualPathName() or GetFullPathName() return the name using casing that was passed in (e.g. returns "program files" if that was passed in, even if it should be "Program Files").

我错过了一些明显的 WinAPI 调用吗?最简单的,如 GetActualPathName() 或 GetFullPathName() 使用传入的大小写返回名称(例如,如果传入,则返回“程序文件”,即使它应该是“程序文件”)。

I'm looking for a native solution (not .NET one).

我正在寻找本机解决方案(不是 .NET 解决方案)。

采纳答案by NeARAZ

And hereby I answer my own question, based on original answer from cspirz.

在此,我根据cspirz 的原始答案回答我自己的问题。

Here's a function that given absolute, relative or network path, will return the path with upper/lower case as it would be displayed on Windows. If some component of the path does not exist, it will return the passed in path from that point.

这是一个给定绝对、相对或网络路径的函数,它将返回大写/小写的路径,就像它在 Windows 上显示的那样。如果路径的某些组件不存在,它将返回从该点传入的路径。

It is quite involved because it tries to handle network paths and other edge cases. It operates on wide character strings and uses std::wstring. Yes, in theory Unicode TCHAR could be not the same as wchar_t; that is an exercise for the reader :)

它非常复杂,因为它试图处理网络路径和其他边缘情况。它对宽字符串进行操作并使用 std::wstring。是的,理论上 Unicode TCHAR 可能与 wchar_t 不同;这是读者的练习:)

std::wstring GetActualPathName( const wchar_t* path )
{
    // This is quite involved, but the meat is SHGetFileInfo

    const wchar_t kSeparator = L'\';

    // copy input string because we'll be temporary modifying it in place
    size_t length = wcslen(path);
    wchar_t buffer[MAX_PATH];
    memcpy( buffer, path, (length+1) * sizeof(path[0]) );

    size_t i = 0;

    std::wstring result;

    // for network paths (\server\share\RestOfPath), getting the display
    // name mangles it into unusable form (e.g. "\server\share" turns
    // into "share on server (server)"). So detect this case and just skip
    // up to two path components
    if( length >= 2 && buffer[0] == kSeparator && buffer[1] == kSeparator )
    {
        int skippedCount = 0;
        i = 2; // start after '\'
        while( i < length && skippedCount < 2 )
        {
            if( buffer[i] == kSeparator )
                ++skippedCount;
            ++i;
        }

        result.append( buffer, i );
    }
    // for drive names, just add it uppercased
    else if( length >= 2 && buffer[1] == L':' )
    {
        result += towupper(buffer[0]);
        result += L':';
        if( length >= 3 && buffer[2] == kSeparator )
        {
            result += kSeparator;
            i = 3; // start after drive, colon and separator
        }
        else
        {
            i = 2; // start after drive and colon
        }
    }

    size_t lastComponentStart = i;
    bool addSeparator = false;

    while( i < length )
    {
        // skip until path separator
        while( i < length && buffer[i] != kSeparator )
            ++i;

        if( addSeparator )
            result += kSeparator;

        // if we found path separator, get real filename of this
        // last path name component
        bool foundSeparator = (i < length);
        buffer[i] = 0;
        SHFILEINFOW info;

        // nuke the path separator so that we get real name of current path component
        info.szDisplayName[0] = 0;
        if( SHGetFileInfoW( buffer, 0, &info, sizeof(info), SHGFI_DISPLAYNAME ) )
        {
            result += info.szDisplayName;
        }
        else
        {
            // most likely file does not exist.
            // So just append original path name component.
            result.append( buffer + lastComponentStart, i - lastComponentStart );
        }

        // restore path separator that we might have nuked before
        if( foundSeparator )
            buffer[i] = kSeparator;

        ++i;
        lastComponentStart = i;
        addSeparator = true;
    }

    return result;
}

Again, thanks to cspirz for pointing me to SHGetFileInfo.

再次感谢 cspirz 将我指向 SHGetFileInfo。

回答by cspirz

Have you tried using SHGetFileInfo?

您是否尝试过使用SHGetFileInfo?

回答by sergioko

There is another solution. First call GetShortPathName() and then GetLongPathName(). Guess what character case will be used then? ;-)

还有另一种解决方案。首先调用 GetShortPathName(),然后调用 GetLongPathName()。猜猜那时将使用什么字符大小写?;-)

回答by bugmagnet

Okay, this is VBScript, but even so I'd suggest using the Scripting.FileSystemObject object

好的,这是 VBScript,但即便如此,我还是建议使用 Scripting.FileSystemObject 对象

Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
Dim f
Set f = fso.GetFile("C:\testfile.dat") 'actually named "testFILE.dAt"
wscript.echo f.Name

The response I get is from this snippet is

我得到的回应来自这个片段

testFILE.dAt

Hope that at least points you in the right direction.

希望至少为您指明正确的方向。

回答by raymai97

Just found that the Scripting.FileSystemObjectsuggested by @bugmagnet 10 years ago is a treasure. Unlike my old method, it works on Absolute Path, Relative Path, UNC Path and Very Long Path (path longer than MAX_PATH). Shame on me for not testing his method earlier.

刚刚发现Scripting.FileSystemObject@bugmagnet 10年前的建议是个宝。与我的旧方法不同,它适用于绝对路径、相对路径、UNC 路径和非常长的路径(路径长于MAX_PATH)。为我没有早点测试他的方法而感到羞耻。

For future reference, I would like to present this code which can be compiled in both C and C++ mode. In C++ mode, the code will use STL and ATL. In C mode, you can clearly see how everything is working behind the scene.

为了将来参考,我想展示这段代码,它可以在 C 和 C++ 模式下编译。在 C++ 模式下,代码将使用 STL 和 ATL。在 C 模式下,您可以清楚地看到幕后的一切工作方式。

#include <Windows.h>
#include <objbase.h>
#include <conio.h> // for _getch()

#ifndef __cplusplus
#   include <stdio.h>

#define SafeFree(p, fn) \
    if (p) { fn(p); (p) = NULL; }

#define SafeFreeCOM(p) \
    if (p) { (p)->lpVtbl->Release(p); (p) = NULL; }


static HRESULT CorrectPathCasing2(
    LPCWSTR const pszSrc, LPWSTR *ppszDst)
{
    DWORD const clsCtx = CLSCTX_INPROC_SERVER;
    LCID const lcid = LOCALE_USER_DEFAULT;
    LPCWSTR const pszProgId = L"Scripting.FileSystemObject";
    LPCWSTR const pszMethod = L"GetAbsolutePathName";
    HRESULT hr = 0;
    CLSID clsid = { 0 };
    IDispatch *pDisp = NULL;
    DISPID dispid = 0;
    VARIANT vtSrc = { VT_BSTR };
    VARIANT vtDst = { VT_BSTR };
    DISPPARAMS params = { 0 };
    SIZE_T cbDst = 0;
    LPWSTR pszDst = NULL;

    // CoCreateInstance<IDispatch>(pszProgId, &pDisp)

    hr = CLSIDFromProgID(pszProgId, &clsid);
    if (FAILED(hr)) goto eof;

    hr = CoCreateInstance(&clsid, NULL, clsCtx,
        &IID_IDispatch, (void**)&pDisp);
    if (FAILED(hr)) goto eof;
    if (!pDisp) {
        hr = E_UNEXPECTED; goto eof;
    }

    // Variant<BSTR> vtSrc(pszSrc), vtDst;
    // vtDst = pDisp->InvokeMethod( pDisp->GetIDOfName(pszMethod), vtSrc );

    hr = pDisp->lpVtbl->GetIDsOfNames(pDisp, NULL,
        (LPOLESTR*)&pszMethod, 1, lcid, &dispid);
    if (FAILED(hr)) goto eof;

    vtSrc.bstrVal = SysAllocString(pszSrc);
    if (!vtSrc.bstrVal) {
        hr = E_OUTOFMEMORY; goto eof;
    }
    params.rgvarg = &vtSrc;
    params.cArgs = 1;
    hr = pDisp->lpVtbl->Invoke(pDisp, dispid, NULL, lcid,
        DISPATCH_METHOD, &params, &vtDst, NULL, NULL);
    if (FAILED(hr)) goto eof;
    if (!vtDst.bstrVal) {
        hr = E_UNEXPECTED; goto eof;
    }

    // *ppszDst = AllocWStrCopyBStrFrom(vtDst.bstrVal);

    cbDst = SysStringByteLen(vtDst.bstrVal);
    pszDst = HeapAlloc(GetProcessHeap(),
        HEAP_ZERO_MEMORY, cbDst + sizeof(WCHAR));
    if (!pszDst) {
        hr = E_OUTOFMEMORY; goto eof;
    }
    CopyMemory(pszDst, vtDst.bstrVal, cbDst);
    *ppszDst = pszDst;

eof:
    SafeFree(vtDst.bstrVal, SysFreeString);
    SafeFree(vtSrc.bstrVal, SysFreeString);
    SafeFreeCOM(pDisp);
    return hr;
}

static void Cout(char const *psz)
{
    printf("%s", psz);
}

static void CoutErr(HRESULT hr)
{
    printf("Error HRESULT 0x%.8X!\n", hr);
}

static void Test(LPCWSTR pszPath)
{
    LPWSTR pszRet = NULL;
    HRESULT hr = CorrectPathCasing2(pszPath, &pszRet);
    if (FAILED(hr)) {
        wprintf(L"Input: <%s>\n", pszPath);
        CoutErr(hr);
    }
    else {
        wprintf(L"Was: <%s>\nNow: <%s>\n", pszPath, pszRet);
        HeapFree(GetProcessHeap(), 0, pszRet);
    }
}


#else // Use C++ STL and ATL
#   include <iostream>
#   include <iomanip>
#   include <string>
#   include <atlbase.h>

static HRESULT CorrectPathCasing2(
    std::wstring const &srcPath,
    std::wstring &dstPath)
{
    HRESULT hr = 0;
    CComPtr<IDispatch> disp;
    hr = disp.CoCreateInstance(L"Scripting.FileSystemObject");
    if (FAILED(hr)) return hr;

    CComVariant src(srcPath.c_str()), dst;
    hr = disp.Invoke1(L"GetAbsolutePathName", &src, &dst);
    if (FAILED(hr)) return hr;

    SIZE_T cch = SysStringLen(dst.bstrVal);
    dstPath = std::wstring(dst.bstrVal, cch);
    return hr;
}

static void Cout(char const *psz)
{
    std::cout << psz;
}

static void CoutErr(HRESULT hr)
{
    std::wcout
        << std::hex << std::setfill(L'0') << std::setw(8)
        << "Error HRESULT 0x" << hr << "\n";
}

static void Test(std::wstring const &path)
{
    std::wstring output;
    HRESULT hr = CorrectPathCasing2(path, output);
    if (FAILED(hr)) {
        std::wcout << L"Input: <" << path << ">\n";
        CoutErr(hr);
    }
    else {
        std::wcout << L"Was: <" << path << ">\n"
            << "Now: <" << output << ">\n";
    }
}

#endif


static void TestRoutine(void)
{
    HRESULT hr = CoInitialize(NULL);

    if (FAILED(hr)) {
        Cout("CoInitialize failed!\n");
        CoutErr(hr);
        return;
    }

    Cout("\n[ Absolute Path ]\n");
    Test(L"c:\uSers\RayMai\docuMENTs");
    Test(L"C:\WINDOWS\SYSTEM32");

    Cout("\n[ Relative Path ]\n");
    Test(L".");
    Test(L"..");
    Test(L"\");

    Cout("\n[ UNC Path ]\n");
    Test(L"\\VMWARE-HOST\SHARED FOLDERS\D\PROGRAMS INSTALLER");

    Cout("\n[ Very Long Path ]\n");
    Test(L"\\?\C:\VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME");

    Cout("\n!! Worth Nothing Behavior !!\n");
    Test(L"");
    Test(L"1234notexist");
    Test(L"C:\bad\PATH");

    CoUninitialize();
}

int main(void)
{
    TestRoutine();
    _getch();
    return 0;
}

Screenshot:

截屏:

screenshot2

截图2



Old Answer:

旧答案:

I found that FindFirstFile()will return the proper casing file name (last part of path) in fd.cFileName. If we pass c:\winDOWs\exPLORER.exeas first parameter to FindFirstFile(), the fd.cFileNamewould be explorer.exelike this:

我发现这FindFirstFile()将返回正确的大小写文件名(路径的最后一部分)fd.cFileName。如果我们将c:\winDOWs\exPLORER.exe第一个参数传递给FindFirstFile()fd.cFileName它将是explorer.exe这样的:

prove

证明

If we replace the last part of path with fd.cFileName, we will get the last part right; the path would become c:\winDOWs\explorer.exe.

如果我们将 path 的最后一部分替换为fd.cFileName,我们将得到最后一部分;路径将成为c:\winDOWs\explorer.exe.

Assuming the path is always absolute path (no change in text length), we can just apply this 'algorithm' to every part of path (except the drive letter part).

假设路径始终是绝对路径(文本长度没有变化),我们可以将此“算法”应用于路径的每个部分(驱动器号部分除外)。

Talk is cheap, here is the code:

谈话很便宜,这是代码:

#include <windows.h>
#include <stdio.h>

/*
    c:\windows\windowsupdate.log --> c:\windows\WindowsUpdate.log
*/
static HRESULT MyProcessLastPart(LPTSTR szPath)
{
    HRESULT hr = 0;
    HANDLE hFind = NULL;
    WIN32_FIND_DATA fd = {0};
    TCHAR *p = NULL, *q = NULL;
    /* thePart = GetCorrectCasingFileName(thePath); */
    hFind = FindFirstFile(szPath, &fd);
    if (hFind == INVALID_HANDLE_VALUE) {
        hr = HRESULT_FROM_WIN32(GetLastError());
        hFind = NULL; goto eof;
    }
    /* thePath = thePath.ReplaceLast(thePart); */
    for (p = szPath; *p; ++p);
    for (q = fd.cFileName; *q; ++q, --p);
    for (q = fd.cFileName; *p = *q; ++p, ++q);
eof:
    if (hFind) { FindClose(hFind); }
    return hr;
}

/*
    Important! 'szPath' should be absolute path only.
    MUST NOT SPECIFY relative path or UNC or short file name.
*/
EXTERN_C
HRESULT __stdcall
CorrectPathCasing(
    LPTSTR szPath)
{
    HRESULT hr = 0;
    TCHAR *p = NULL;
    if (GetFileAttributes(szPath) == -1) {
        hr = HRESULT_FROM_WIN32(GetLastError()); goto eof;
    }
    for (p = szPath; *p; ++p)
    {
        if (*p == '\' || *p == '/')
        {
            TCHAR slashChar = *p;
            if (p[-1] == ':') /* p[-2] is drive letter */
            {
                p[-2] = toupper(p[-2]);
                continue;
            }
            *p = '##代码##';
            hr = MyProcessLastPart(szPath);
            *p = slashChar;
            if (FAILED(hr)) goto eof;
        }
    }
    hr = MyProcessLastPart(szPath);
eof:
    return hr;
}

int main()
{
    TCHAR szPath[] = TEXT("c:\windows\EXPLORER.exe");
    HRESULT hr = CorrectPathCasing(szPath);
    if (SUCCEEDED(hr))
    {
        MessageBox(NULL, szPath, TEXT("Test"), MB_ICONINFORMATION);
    }
    return 0;
}

prove 2

证明 2

Advantages:

好处:

  • The code works on every version of Windows since Windows 95.
  • Basic error-handling.
  • Highest performance possible. FindFirstFile()is very fast, direct buffer manipulation makes it even faster.
  • Just C and pure WinAPI. Small executable size.
  • 该代码适用于自 Windows 95 以来的每个版本的 Windows。
  • 基本的错误处理。
  • 可能的最高性能。FindFirstFile()非常快,直接缓冲区操作使它更快。
  • 只是 C 和纯 WinAPI。小的可执行文件大小。

Disadvantages:

缺点:

  • Only absolute path is supported, other are undefined behavior.
  • Not sure if it is relying on undocumented behavior.
  • The code might be too raw too much DIY for some people. Might get you flamed.
  • 仅支持绝对路径,其他都是未定义的行为。
  • 不确定它是否依赖于无证行为。
  • 对于某些人来说,代码可能过于原始太多 DIY。可能会让你发火。

Reason behind the code style:

代码风格背后的原因:

I use gotofor error-handling because I was used to it (gotois very handy for error-handling in C). I use forloop to perform functions like strcpyand strchron-the-fly because I want to be certain what was actually executed.

goto用于错误处理,因为我已经习惯了(goto对于 C 中的错误处理非常方便)。我使用for循环来执行类似strcpystrchr即时的功能,因为我想确定实际执行了什么。

回答by Doub

FindFirstFileNameWwill work with a few drawbacks:

FindFirstFileNameW将有一些缺点:

  • it doesn't work on UNC paths
  • it strips the drive letter so you need to add it back
  • if there are more than one hard link to your file you need to identify the right one
  • 它不适用于 UNC 路径
  • 它会删除驱动器号,因此您需要将其添加回来
  • 如果您的文件有多个硬链接,您需要确定正确的链接

回答by Adam Rosenfield

After a quick test, GetLongPathName()does what you want.

经过快速测试后,GetLongPathName() 会执行您想要的操作。