Windows 上带有通配符的目录中的文件

Files in directory with wildcard on Windows

本文关键字:文件 通配符 Windows      更新时间:2023-10-16

如何轻松地从包含通配符的路径获取所有文件路径?例如:C:/Data*Set/Files*/*.txt,我使用glob函数在Linux上编写了它,但我无法在Windows上执行此操作:/

不幸的是,FindFirstFile不支持目录名称中的通配符。

我认为应该有一个可用的 Windows 解决方案,但我找不到它。

因此,您应该放弃使用特定于操作系统的文件访问,而支持独立于操作系统的文件访问: 文件系统库

假设为您提供了包含通配符路径的filesystem::path input。要使用它来解决您的问题,您需要:

  1. 使用parent_pathinput分解为目录
  2. 使用filename获取input文件名
  3. 获取input开始的相对路径或绝对路径的directory_iterator
  4. 创建一个递归函数,该函数接收begin并将迭代器end到获得的父路径、目录迭代器和文件名
  5. 每当目录或文件名使用'*'使用迭代器的regex来确定应前进到下一个目录
  6. 返回匹配文件的路径或空path

由于出色的 Ben Voigt 评论,我更新了算法以跳过未通配符的目录。

例如:

regex GenerateRegex(string& arg) {
for (auto i = arg.find('*'); i != string::npos; i = arg.find('*', i + 2)) {
arg.insert(i, 1, '.');
}
return regex(arg);
}
filesystem::path FindFirstFile(filesystem::path directory, filesystem::path::const_iterator& start, const filesystem::path::const_iterator& finish, string& filename) {
while (start != finish && start->string().find('*') == string::npos) {
directory /= *start++;
}
filesystem::directory_iterator it(directory);
filesystem::path result;
if (it != filesystem::directory_iterator()) {
if (start == finish) {
for (auto i = filename.find('.'); i != string::npos; i = filename.find('.', i + 2)) {
filename.insert(i, 1, '');
}
const auto re = GenerateRegex(filename);
do {
if (!filesystem::is_directory(it->status()) && regex_match(it->path().string(), re)) {
result = *it;
break;
}
} while (++it != filesystem::directory_iterator());
}
else {
const auto re = GenerateRegex(start->string());
do {
if (it->is_directory() && regex_match(prev(it->path().end())->string(), re)) {
result = FindFirstFile(it->path(), next(start), finish, filename);
if (!result.empty()) {
break;
}
}
} while (++it != filesystem::directory_iterator());
}
}
return result;
}

可以调用:

const filesystem::path input("C:/Test/Data*Set/Files*/*.txt");
if (input.is_absolute()) {
const auto relative_parent = input.parent_path().relative_path();
cout << FindFirstFile(input.root_path(), begin(relative_parent), end(relative_parent), input.filename().string()) << endl;
} else {
const auto parent = input.parent_path();
cout << FindFirstFile(filesystem::current_path(), begin(parent), end(parent), input.filename().string()) << endl;
}

现场示例

需要了解FindFirstFile[Ex]是如何工作的。 这是壳NtQueryDirectoryFile.FindFirstFile[Ex]需要将输入名称划分为文件夹名称(将用作FileHandle打开)并使用用作文件名的搜索掩码。掩码只能出现在文件名中。文件夹必须具有不带通配符的确切名称才能首先打开。

因此FindFirstFile[Ex]始终打开具体的单个文件夹并按掩码在此文件夹中搜索。 对于递归搜索文件 - 我们需要递归调用FindFirstFile[Ex]。 简单地说,我们在所有级别上都使用相同的常量搜索掩码。 例如,当我们想要查找所有文件从X:SomeFolder开始时,我们首先调用FindFirstFile[Ex]X:SomeFolder*在级别 0。 如果我们找到SomeSubfolder- 我们调用FindFirstFile[Ex]X:SomeFolderSomeSubfolder*在水平上1 等。 但是我们可以在不同的级别上使用不同的搜索掩码Data*Set级 0、Files*级 1 级、*.txt级 2

级所以我们需要递归FindFirstFileEx调用,在不同的递归级别使用不同的掩码。 例如,我们希望找到c:Program***.txt. 我们需要从c:Program*开始,然后对于每个建立的结果附加掩码*,然后将*.txt附加到下一个级别。 或者我们可以例如想要下一个 - 按下一个掩码搜索文件 -c:Program Files*Internet Explorer*任何深层次。 我们可以将常量深度搜索文件夹掩码(可选)与最终掩码(也可选)已经在所有更深的级别上使用。 所有这些都可以真正实现得不那么困难和高效:

struct ENUM_CONTEXT : WIN32_FIND_DATA 
{
PCWSTR _szMask;
PCWSTR *_pszMask;
ULONG _MaskCount;
ULONG _MaxLevel;
ULONG _nFiles;
ULONG _nFolders;
WCHAR _FileName[MAXSHORT + 1];
void StartEnum(PCWSTR pcszRoot, PCWSTR pszMask[], ULONG MaskCount, PCWSTR szMask, ULONG MaxLevel, PSTR prefix)
{
SIZE_T len = wcslen(pcszRoot);
if (len < RTL_NUMBER_OF(_FileName))
{
memcpy(_FileName, pcszRoot, len * sizeof(WCHAR));
_szMask = szMask, _pszMask = pszMask, _MaskCount = MaskCount;
_MaxLevel = szMask ? MaxLevel : MaskCount;
_nFolders = 0, _nFolders = 0;
Enum(_FileName + len, 0, prefix);
}
}
void Enum(PWSTR pszEnd, ULONG nLevel, PSTR prefix);
};
void ENUM_CONTEXT::Enum(PWSTR pszEnd, ULONG nLevel, PSTR prefix)
{
if (nLevel > _MaxLevel)
{
return ;
}
PCWSTR lpFileName = _FileName;
SIZE_T cb = lpFileName + RTL_NUMBER_OF(_FileName) - pszEnd;
PCWSTR szMask = nLevel < _MaskCount ? _pszMask[nLevel] : _szMask;
SIZE_T cchMask = wcslen(szMask) + 1;
if (cb < cchMask + 1)
{
return ;
}
*pszEnd++ = L'', cb--;
DbgPrint("%s[<%.*S>]n", prefix, pszEnd - lpFileName, lpFileName);
memcpy(pszEnd, szMask, cchMask * sizeof(WCHAR));
ULONG dwError;
HANDLE hFindFile = FindFirstFileEx(lpFileName, FindExInfoBasic, this, FindExSearchNameMatch, 0, FIND_FIRST_EX_LARGE_FETCH);
if (hFindFile != INVALID_HANDLE_VALUE)
{
PWSTR FileName = cFileName;
do 
{
SIZE_T FileNameLength = wcslen(FileName);
switch (FileNameLength)
{
case 2:
if (FileName[1] != '.') break;
case 1:
if (FileName[0] == '.') continue;
}
if (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
_nFolders++;
if (!(dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
{
if (cb < FileNameLength)
{
__debugbreak();
}
else
{
memcpy(pszEnd, FileName, FileNameLength * sizeof(WCHAR));
Enum(pszEnd + FileNameLength, nLevel + 1, prefix - 1);
}
}
}
else if (nLevel >= _MaskCount || (!_szMask && nLevel == _MaskCount - 1))
{
_nFiles++;
DbgPrint("%s%u%u <%.*S>n", prefix, nFileSizeLow, nFileSizeHigh, FileNameLength, FileName);
}
} while (FindNextFile(hFindFile, this));
if ((dwError = GetLastError()) == ERROR_NO_MORE_FILES)
{
dwError = NOERROR;
}
FindClose(hFindFile);
}
else
{
dwError = GetLastError();
}
if (dwError && dwError != ERROR_FILE_NOT_FOUND)
{
DbgPrint("%s[<%.*S>] err = %un", prefix, pszEnd - lpFileName, lpFileName, dwError);
}
}
void Test(PCWSTR pcszRoot)
{
char prefix[MAXUCHAR + 1];
memset(prefix, 't', RTL_NUMBER_OF(prefix) - 1);
prefix[RTL_NUMBER_OF(prefix) - 1] = 0;
ENUM_CONTEXT ectx;
static PCWSTR Masks[] = { L"Program*", L"*", L"*.txt" };
static PCWSTR Masks2[] = { L"Program*", L"*" };
static PCWSTR Masks3[] = { L"Program Files*", L"Internet Explorer" };
// search Program***.txt with fixed deep level
ectx.StartEnum(pcszRoot, Masks, RTL_NUMBER_OF(Masks), 0, RTL_NUMBER_OF(prefix) - 1, prefix + RTL_NUMBER_OF(prefix) - 1);
// search *.txt files from Program** - any deep level
ectx.StartEnum(pcszRoot, Masks2, RTL_NUMBER_OF(Masks2), L"*.txt", RTL_NUMBER_OF(prefix) - 1, prefix + RTL_NUMBER_OF(prefix) - 1);
// search all files (*) from Program Files*Internet Explorer 
ectx.StartEnum(pcszRoot, Masks3, RTL_NUMBER_OF(Masks3), L"*", RTL_NUMBER_OF(prefix) - 1, prefix + RTL_NUMBER_OF(prefix) - 1);
}