65.9K
CodeProject 正在变化。 阅读更多。
Home

递归模式化文件通配符

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (20投票s)

2002年8月28日

9分钟阅读

viewsIcon

191484

downloadIcon

2019

用于根据通配符模式递归或非递归地匹配文件或目录的类和应用程序。

引言

Perforce 最让我喜欢的功能之一是其递归通配符语法。我每天都使用它。我无需在 GUI 或命令行中的目录树中导航来查找特定文件以执行操作,只需使用 Perforce 的 '...' 递归语法即可找到文件并签出。

p4 edit ...MyFile.cpp

作为更专业的功能,'...' 语法也可以接受扩展名。

p4 edit ....h

这会签出当前目录下所有子目录中的所有 *.h 文件。

我希望在自己的应用程序中实现类似的功能,因此我开始在互联网上搜索任何实现此行为的现有代码。我偶然发现了 DJGPP 的 glob() 函数(OpenBSD 中也有)。结合 POSIX 函数 fnmatch(),它们是绝佳的组合,并且几乎完全按照我想要的方式工作。唯一的限制是许可证。GPL 不适合许多环境,尽管我发布了我大部分产品的源代码,但我无法承担我编写的所有代码都受 GPL 许可。

所以,又回到了搜索。接下来,我发现了 Matthias Wandel 的 MyGlob 代码,来自他的 Exif Jpeg 相机设置解析器和缩略图删除器应用程序(请参阅“致谢”中的 URL)。它的行为大部分是我想要的,而且它不受某些需要你“卖灵魂”的许可证约束。事实上,它没有任何许可证,并且可以自由使用。

在将他的代码的一个版本修改为我喜欢的方式后,我给他发了电子邮件,告诉他我做了什么,并得到了他在这里发布我的修改版本的许可。总之,Matthias 编写了原始实现。我添加了一些新功能,下面我将讨论整个产品。

此代码的最新版本可以在 http://workspacewhiz.com/ 的“杂项代码”部分找到。

Globbing?

说实话,我也很惊讶。如果我早知道“glob”就是我正在寻找的东西,我的互联网搜索会更快。我一直认为 glob 是我在大学时室友做的那些黏糊糊的晚餐,但我想我大错特错了。:)

在此情况下,文件 glob 是通过模式匹配的零个或多个文件名,可能嵌入了通配符。

模式和通配符

如果不指定路径,匹配(即 globbing)将从当前目录开始。

通配符 描述
? 匹配文件名或目录名称中的任何单个字符。
* 匹配文件名或目录名称中的 0 个或多个字符。
模式末尾的 '/' 带有结尾斜杠的任何模式都将启动目录搜索,而不是默认的文件搜索。
** 与我想要使用 Perforce 风格的 '...' 递归语法相反,Matthias 提出了一个重要观点。使用 4DOS (http://www.jpsoft.com/) 的用户习惯于 '...' 表示 '..\..\'。经过思考,我认为 Matthias 原始的 '**' 递归语法是一个更好的解决方案。

以下是一些示例

示例模式 描述
File.txt 匹配名为 File.txt 的文件或目录。
File*.txt 匹配所有以 File 开头并以 .txt 扩展名结尾的文件或目录。
File?.txt 匹配所有以 File 开头并包含一个额外字符的文件或目录。
F??e*.txt 匹配一个以 F 开头,后跟任意两个字符,然后是 e,然后是任意数量字符直到 .txt 扩展名的文件或目录。
File* 匹配以 File 开头并以或不以扩展名结尾的文件或目录。
* 匹配所有文件(非递归)。
*/ 匹配所有目录(非递归)。
A*/ 匹配以 A 开头的任何目录(非递归)。
**/* 匹配所有文件(递归)。
** 上述的简写形式。匹配所有文件(递归)。内部展开为 **/*
**/ 匹配所有目录(递归)。
**{文件名字符}** 递归匹配 {文件名字符}。内部展开为 .../*{文件名字符}。
{目录名字符}** 展开为 {目录名字符}*/**。
{目录名字符}**{文件名字符} 展开为 {目录名字符}*/**/*{文件名字符}。
**.h 递归匹配所有 *.h 文件。展开为 **/*.h。
**resource.h 递归匹配所有 *resource.h 文件。展开为 .../*resource.h。
BK** 递归匹配以 BK 开头的任何目录中的所有文件。展开为 BK*/**。
BK**.h 递归匹配以 BK 开头的任何目录中的所有 *.h 文件。展开为 BK*/**/*.h。
c:/Src/**/*.h 从 c:/Src/ 开始,递归匹配所有 *.h 文件。
c:/Src/**/*Grid/ 递归匹配 c:/Src/ 下所有以 Grid 结尾的目录。
c:/Src/**/*Grid*/ 递归匹配 c:/Src/ 下所有包含 Grid 的目录。
c:/Src/**/*Grid*/**/ABC/**/Readme.txt 递归匹配 c:/Src/ 下所有包含 Grid 的目录。从找到的目录开始,递归匹配目录直到找到 ABC/。从那里,递归搜索文件 Readme.txt

最后,还有一些可用的标志。标志附加在模式行的末尾。每个标志都以 @ 字符开头。除非空格是字符串文字的一部分,否则不应插入空格。

标志和其他扩展 描述
@-模式 pattern 添加到忽略列表中。任何匹配忽略列表中模式的文件都将被排除在搜索之外。
@=模式 pattern 添加到排除文件列表中。任何不匹配排除文件列表中模式的文件都将被自动从搜索中删除。
超过两个句点用于向上追溯父目录。 类似于 4DOS,每超过两个句点就向上追溯一个额外的父目录。因此,一个 4 句点的路径展开为 ../../../.

再举几个例子

示例模式 描述
Src/**/@-SCCS/@-BitKeeper/ 递归列出 Src/ 下的所有目录,但 SCCS/ 和 BitKeeper/ 目录将被过滤掉。
Src/**@=*.lua@=README 递归列出 Src/ 下所有匹配 *.lua 或 README 的文件。所有其他文件将被忽略。
Src/**/@-SCCS/@-BitKeeper/@=*.lua@=README 递归列出 Src/ 下所有匹配 *.lua 或 README 的文件。可能存在于 SCCS/ 或 BitKeeper/ 中的那些文件的版本将被忽略。

匹配文件

FileGlobBase 是所有 glob 操作的基类。无法实例化 FileGlobBase。有一个必须重写的抽象函数,名为 FoundMatch()。每当找到匹配项时,都会调用 FoundMatch() 并传入匹配的名称。

如果我们想在接收到名称时将它们打印到 stdout,我们会创建一个派生类,如下所示

class FileGlobPrintStdout : public FileGlobBase
{
    virtual void FoundMatch( const char* name )
    {
        printf( "%s\n", name );
    }
};

接下来,我们实例化对象

FileGlobPrintStdout fileGlob;

要开始匹配过程,将调用函数 FileGlobBase::MatchPattern() 并传入请求的模式。

fileGlob.MatchPattern( "**" );

MatchPattern() 退出时,当前目录及其子目录中的所有文件都将已传递到 FileGlobPrintStdout::FoundMatch() 并打印到 stdout

忽略文件和目录

几个源代码控制系统会在工作副本中添加额外的目录。例如,CVS 会在工作副本的每个目录中添加一个名为 CVS/ 的目录。BitKeeper 会在工作副本的根目录中添加一个名为 BitKeeper/ 的目录,并在源代码控制下的每个目录中添加名为 SCCS/ 的目录。

文件 globbing 类为此问题提供了一个简单的解决方案。可以调用 FileGlobBase::AddIgnorePattern() 并传入一个模式(带通配符或不带),任何匹配该模式的文件或目录都将被忽略。此功能直接对应于文件模式的 @- 标志,如上所述。

目录忽略模式指定为 Dir/。必须存在结尾斜杠。Dir/Dir 是两个不同的模式,前者指目录,后者指文件。

为了从递归列表中删除所有 CVS 目录,我们调用

fileGlob.AddIgnorePattern( "CVS/" );
fileGlob.MatchPattern( "**" );

这种方法同样适用于文件。如果所需文件列表应包含 MP3 文件而不包含 WAV 文件,我们可以插入通配符 *.wav

fileGlob.AddIgnorePattern( "*.wav" );
fileGlob.MatchPattern( "**" );

显然,匹配模式 *.wav 并忽略模式 *.wav 将导致不列出任何文件。

强制仅包含某些文件

FileGlobBase 实现了一个名为 AddExclusivePattern() 的函数。提供排除模式可确保您的应用程序仅通过 FoundMatch() 接收匹配任何已注册排除模式的文件。此功能直接对应于文件模式标志 @=,如上所述。

fileGlob.AddExclusivePattern( "*.lua" );
fileGlob.AddExclusivePattern( "*.c" );
fileGlob.MatchPattern( "**" );

在递归匹配 ** 时,仅考虑匹配 *.lua*.c 的文件。

类详细信息

存档中包含的类按照 Doxygen 约定进行了文档化。源代码和头文件中存在大量的文档。

类:FileGlobBase

FileGlobBase 是所有文件 glob 访问的基类。无法实例化 FileGlobBase 类。必须使用实现 FoundMatch() 的派生类。

类:FileGlobList

FileGlobList 派生自 FileGlobBase 并混合了 std::list< std::string > 容器。FileGlobListFoundMatch() 提供了实现,并将匹配的文件列表存储在 std::list<> 容器中。std::list<> 的所有基本 STL 操作都可以直接在 FileGlobList 上使用。

对于发送到 FoundMatch() 的每个文件,容器都会使用不区分大小写的搜索进行迭代。如果文件已在容器中,则忽略它。否则,将其附加到末尾。这样,可以多次调用 MatchPattern() 函数来累积大量唯一文件。应该注意的是,后续调用 MatchPattern() 可能会将文件插入到容器中,顺序不正确。

示例:Glob 应用程序

包含一个展示 globbing 功能的示例应用程序。示例 Visual Studio .NET 解决方案 Glob.sln 构建了可执行文件 Glob.exeGlob.exe 是文件 glob 代码的简化命令行接口。

可以在不带参数(或使用 -?)的情况下运行 Glob.exe 来查看其用法。

对于支持重定向的应用程序,可以将 Glob.exe 的输出重定向到另一个应用程序。如果使用 BitKeeper 并希望编辑所有 *.cpp 文件和 *.h 文件,用户将运行

glob -i SCCS/ -i BitKeeper/ **.cpp **.h | bk edit -

-i 命令行选项用于指定忽略模式。不应考虑 SCCS/BitKeeper/ 目录中的内容。

最后,可以通过命令行标志 -e 指定排除模式。这些与上面描述的 @= 标志条目类似。

glob -e *.cpp -e *.h **

愿望清单

  • 如果能找到 POSIX fnmatch() 函数的免费实现并替换当前使用的 WildMatch() 例程,那将是极好的。fnmatch() 提供了更强的“正则表达式”风格的匹配功能。

已知bug

匹配的组合有很多。我已经尝试了很多组合,并且这些组合似乎有效。如果您遇到问题,请告诉我。出现不起作用的情况并不奇怪。如有问题,请随时通过 jjensen@workspacewhiz.com 与我联系。

致谢

  • Perforce (http://www.perforce.com/),最初给我这个想法。对于那些不知道的人来说,Perforce 为其源代码控制软件提供免费的两人许可证。我目前在家中使用 Perforce 来满足我所有的源代码控制需求。它并不完美,但它的效果比大多数都好。
  • Matthias Wandel,提供 MyGlob() 代码,这是驱动文件 globber 的算法基础。原始 C 实现存在于他的 Exif Jpeg 相机设置解析器和缩略图删除器应用程序的 myglob.c 中,网址为 http://www.sentex.net/~mwandel/jhead/
  • Jack Handy,感谢他在 https://codeproject.org.cn/string/wildcmp.asp 的文章。wildcmp() 被扩展为 WildMatch(),允许区分大小写和不区分大小写的比较。
© . All rights reserved.