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

问题描述 投票:0回答:8

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

我知道的一些方法,所有这些看起来都很倒退:

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

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

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

c++ c windows winapi
8个回答
4
投票

在此,我根据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; }

再次感谢 cspirz 将我指向 SHGetFileInfo。


3
投票
您尝试过使用

SHGetFileInfo吗?


2
投票
刚刚发现@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
 循环来即时执行 
strcpy
strchr
 等函数,因为我想确定实际执行的内容。


1
投票
还有另一种解决方案。首先调用 GetShortPathName(),然后调用 GetLongPathName()。猜猜接下来会使用什么字符大小写? ;-)


0
投票
好吧,这是 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

我从这段代码中得到的回应是

testFILE.dAt

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


0
投票

FindFirstFileNameW 也有一些缺点:

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

0
投票
这是一种使用 CreateFile 和 GetFinalPathNameByHandle 的方法。

甚至

Rustcpython也使用这些调用来获取真实的文件路径。

#include <iostream> #include <windows.h> /** * @brief Normalize the case and format of a file path. * * @param filePath It will be updated with the normalized file path. * @param length It will be updated with the length of the normalized file path. * The lenght doesn't include the null characer * * @return Returns true if the normalization is successful, false otherwise. */ bool normalizeFilePathCase(WCHAR** filePath, DWORD* length) { if (filePath == NULL || length == NULL) { return false; } HANDLE hfile = CreateFile(*filePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hfile == INVALID_HANDLE_VALUE) { return false; } DWORD normalizedPathLength = GetFinalPathNameByHandle(hfile, nullptr, 0, FILE_NAME_OPENED); if (!normalizedPathLength) { CloseHandle(hfile); return false; } WCHAR* normalizedPath = new WCHAR[normalizedPathLength + 1]; if (normalizedPath == NULL) { CloseHandle(hfile); return false; } if (!GetFinalPathNameByHandle(hfile, normalizedPath, normalizedPathLength + 1, FILE_NAME_OPENED)) { CloseHandle(hfile); return false; } if (wcsncmp(normalizedPath, L"\\\\?\\UNC\\", 8) == 0) { wmemmove(normalizedPath + 2, normalizedPath + 8, normalizedPathLength - 7); normalizedPathLength -= 6; } else if (wcsncmp(normalizedPath, L"\\\\?\\", 4) == 0) { wmemmove(normalizedPath, normalizedPath + 4, normalizedPathLength - 3); normalizedPathLength -= 4; } delete[] *filePath; CloseHandle(hfile); *filePath = normalizedPath; *length = normalizedPathLength; return true; } int main() { WCHAR filePath[] = L"c:\\windows\\FONTS\\arial.TTF"; WCHAR* filePathReal = new WCHAR[sizeof(filePath)]; wmemcpy(filePathReal, filePath, sizeof(filePath)); bool success; DWORD length; std::wcout << L"Original path: " << filePathReal << std::endl; success = normalizeFilePathCase(&filePathReal, &length); if (!success) { return 1; } std::wcout << L"Modified path: " << filePathReal << std::endl; return 0; }
    

-3
投票
经过快速测试,

GetLongPathName() 可以满足您的要求。

© www.soinside.com 2019 - 2024. All rights reserved.