【问题标题】:Getting actual file name (with proper casing) on Windows在 Windows 上获取实际文件名(使用正确的大小写)
【发布时间】:2023-03-29 16:36:01
【问题描述】:

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

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

  1. 给定完整路径,搜索路径上的每个文件夹(通过 FindFirstFile)。这给出了每个文件夹的正确大小写结果。在最后一步,搜索文件本身。
  2. 从句柄中获取文件名(如MSDN example)。这需要打开一个文件、创建文件映射、获取它的名称、解析设备名称等。相当复杂。它不适用于文件夹或零大小文件。

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

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

【问题讨论】:

    标签: c++ c windows winapi


    【解决方案1】:

    好的,这是 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
    

    我从这个 sn-p 得到的响应是

    testFILE.dAt
    

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

    【讨论】:

      【解决方案2】:

      您是否尝试过使用SHGetFileInfo?

      【讨论】:

      • 是的,这行得通。它只返回最后一个路径名组件(即整个路径中的最终文件夹或文件名),但至少比使用 FindFirstFile 简单得多。
      【解决方案3】:

      经过快速测试,GetLongPathName() 可以满足您的需求。

      【讨论】:

      • 不起作用。 GetLongPathName() 返回传入的任何大小写(“c:\program files”将返回全部小写,文件名相同)。
      【解决方案4】:

      在此我根据original answer from cspirz回答我自己的问题。

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

      它非常复杂,因为它试图处理网络路径和其他边缘情况。它对宽字符串进行操作并使用 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;
      }
      

      再次感谢 cspiz 将我指向 SHGetFileInfo。

      【讨论】:

      • SHGetFileInfo: "SHGFI_DISPLAYNAME: 检索文件的显示名称,即在 Windows 资源管理器中显示的名称。[...] 请注意,显示名称可能会受到设置的影响,例如是否显示扩展名。"您确定这是您要查找的内容吗?
      • 对于 Windows 8 上的文件夹 C:\Users\YourAccountName\Documents,此 将不起作用:它将返回 C:\Users\YourAccountName\My Documents,因为“文档”文件夹在 Windows 资源管理器中显示为“我的文档” .
      • 我没有对答案发表评论的声誉,所以这是我给使用 GetAbsolutePathName 的用户的说明:在引擎盖下 GetAbsolutePathName 切片路径并在路径的部分上使用 FindFirstFile(W) 来查找找出正确的字符大小写,这是一个证明:!enter image description here
      【解决方案5】:

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

      【讨论】:

      • 如果在文件系统上禁用短文件名,这是否有效?
      • @HarryJohnston 不,它在这种情况下不起作用:1)disable short file name creation; 2)创建新文件; 3) 尝试访问该文件的短名称。如果文件是在短文件名创建被禁用之前创建的,它可能仍然有效。
      • 这不起作用,因为路径可能根本没有短文件名,例如。 "C:\Users\Public"。
      • @GlennMaynard 它确实有效。由于某种原因,GetLongPathName() 在实际情况下返回了已经很短的组件。
      【解决方案6】:

      FindFirstFileNameW 有一些缺点:

      • 它不适用于 UNC 路径
      • 它会删除驱动器号,因此您需要将其添加回来
      • 如果您的文件有多个硬链接,您需要找出正确的一个

      【讨论】:

        【解决方案7】:

        刚刚发现@bugmagnet 10 年前推荐的Scripting.FileSystemObject 是宝。与我的旧方法不同,它适用于绝对路径、相对路径、UNC 路径和超长路径(路径长于MAX_PATH)。可惜我没有早点测试他的方法。

        为了将来的参考,我想展示这段代码,它可以在 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;
        }
        

        截图:


        旧答案:

        我发现FindFirstFile() 将在fd.cFileName 中返回正确的外壳文件名(路径的最后一部分)。如果我们将c:\winDOWs\exPLORER.exe 作为第一个参数传递给FindFirstFile(),则fd.cFileName 将是explorer.exe,如下所示:

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

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

        说话很便宜,这里是代码:

        #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 = '\0';
                    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;
        }
        

        优点:

        • 该代码适用于自 Windows 95 以来的所有 Windows 版本。
        • 基本错误处理。
        • 可能的最高性能。 FindFirstFile() 非常快,直接缓冲区操作使其更快。
        • 只有 C 和纯 WinAPI。小可执行文件大小。

        缺点:

        • 仅支持绝对路径,其他为未定义行为。
        • 不确定它是否依赖于未记录的行为。
        • 对于某些人来说,代码可能太原始太 DIY。可能会让你火上浇油。

        代码风格背后的原因:

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

        【讨论】:

        • 不幸的是,在生产代码中,您绝对不能假设路径长度不会改变。操作系统完全支持输入具有多个连续路径分隔符的文件等情况,但会导致您的代码返回错误的路径。
        猜你喜欢
        • 1970-01-01
        • 2021-12-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-01-18
        • 1970-01-01
        • 2016-01-10
        • 2012-10-16
        相关资源
        最近更新 更多