Windows 文件系统不区分大小写。给定一个文件/文件夹名称(例如“somefile”),我如何获得该文件/文件夹的实际名称(例如,如果资源管理器如此显示,它应该返回“SomeFile”)?
我知道的一些方法,所有这些看起来都很倒退:
我是否错过了一些明显的 WinAPI 调用?最简单的,如 GetActualPathName() 或 GetFullPathName() 使用传入的大小写返回名称(例如,如果传入了“程序文件”,即使它应该是“程序文件”)。
我正在寻找一种本机解决方案(不是 .NET 解决方案)。
在此,我根据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。
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, ¶ms, &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;
}
优点:
FindFirstFile()
非常快,直接缓冲区操作使其更快。
我使用
goto
进行错误处理,因为我已经习惯了(
goto
对于 C 中的错误处理非常方便)。我使用
for
循环来即时执行
strcpy
和
strchr
等函数,因为我想确定实际执行的内容。
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
希望至少能为您指明正确的方向。
FindFirstFileNameW 也有一些缺点:
甚至
Rust和cpython也使用这些调用来获取真实的文件路径。
#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;
}
GetLongPathName() 可以满足您的要求。