Simple Windows / DOS style wildcard matcher for Delphi, with a couple of
useful extensions (# for digits, quoted-string alternation inside [...]).
Pure Pascal, single unit, no dependencies beyond the RTL.
| Token | Matches | Example pattern | Matches | Does not match |
|---|---|---|---|---|
* |
Zero or more characters | wh* |
what, why |
awhile, watch |
? |
Exactly one character | b?ll |
ball, bell |
bll, baal |
# |
Exactly one decimal digit (0-9) | 1#3 |
103, 113 |
1a3, 13 |
[abc] |
Any one character in the set | b[ae]ll |
ball, bell |
bill |
[!abc] |
Any one character NOT in the set | b[!ae]ll |
bill, bull |
ball, bell |
[a-z] |
Any one character in the range (low..high, ascending) | b[a-c]d |
bad, bbd |
bdd, b0d |
["foo"|"bar"] |
Any ONE of the listed literal strings at this position | *["3rdparty"|"ThirdParty"]*.md |
docs/3rdparty/x.md, src/ThirdPartyReadme.md |
docs/internal/x.md |
[!"foo"|"bar"] |
A slice of length max(altLen) that is NEITHER prefix |
[!"foo"|"bar"]* |
quxyz |
fooxyz, barxyz |
Sets may combine literals and ranges, e.g. [a-zA-Z0-9_].
A literal ] inside a single-char class must be the first content character:
[]abc] matches ], a, b or c.
A - that is the first or last content character is treated as a literal,
e.g. [-x] matches - or x; [a-] matches a or -.
** collapses to *.
[...] is auto-detected:
- If the first content character (after an optional
!) is", the class is parsed as quoted-string alternation:["foo"|"bar"|"baz"]. The|separates alternatives. The class matches the FIRST alternative that succeeds at the current position and consumes its length. - Otherwise the class follows the legacy single-character rules
(
[abc],[a-z],[!xyz]) and consumes exactly one character.
An empty alternative "" is allowed and matches zero characters
([""|"foo"] will match either nothing or foo).
Quoted alternation is intended for file-mask use. Backslash escapes are
NOT supported - none are needed because Windows file names cannot contain
the characters [, ], | or " to begin with, so the syntax stays
safe to embed in literal masks.
Negated alternation [!"foo"|"bar"] succeeds when NONE of the listed
alternatives is a prefix at the current position; it then consumes the
length of the LONGEST alternative. When the input has fewer characters
left than the longest alternative, the match fails.
| OUTSIDE of [...] has no special meaning and is treated as a literal
character.
Matching is case-insensitive by default (Windows convention).
Pass ACaseSensitive = True for ordinal comparison.
uses
Delphi.WildCardMatcher;TWildCard.Create (no patterns) gives you an empty matcher you can use
for ad-hoc one-shot calls. Case-sensitivity is set at Create time.
if TWildCard.Create.Match(AFileName, '*.pas') then
// ...
// Case-sensitive
if TWildCard.Create(True).Match('Unit1.PAS', '*.pas') then
// ... will NOT match because of the trailing-case differenceWhen you match many inputs against the same fixed pattern set, register
the patterns at Create so the per-call upper-casing happens once. Then
Match(input) walks the registered set short-circuiting on the first hit.
var
LMask: TWildCard;
begin
LMask := TWildCard.Create(['*.pas', '*.dpr', '*.dpk', '*.inc']);
for var LFile in TDirectory.GetFiles(ARoot) do
if LMask.Match(LFile) then
AddToProjectFileList(LFile);
end;The constructor accepts a single pattern, a TArray<string>, or a
TStrings (handy for patterns loaded from a TStringList /
Memo.Lines / .ini file):
var
LMasks: TStringList;
LIgnore: TWildCard;
begin
LMasks := TStringList.Create;
try
LMasks.LoadFromFile('ignore-masks.txt');
LIgnore := TWildCard.Create(LMasks);
for var LFile in TDirectory.GetFiles(ARoot) do
if not LIgnore.Match(LFile) then
ProcessFile(LFile);
finally
LMasks.Free;
end;
end;You can pass an extra one-off pattern to an existing instance. By
default only that pattern is tried; pass True as the third argument
to also try the registered set.
LMask.Match(LFile, '*.dproj'); // only the ad-hoc pattern
LMask.Match(LFile, '*.dproj', True); // ad-hoc + registered setQuoted alternation collapses several "same shape, different word" patterns into a single mask. Instead of:
LMask := TWildCard.Create(['*3rdparty*.md', '*ThirdParty*.md']);you can write:
LMask := TWildCard.Create('*["3rdparty"|"ThirdParty"]*.md');The negated form is handy for "skip files whose name contains any of these tokens":
if TWildCard.Create.Match(LFile, '*[!"backup"|"draft"|"old"]*.docx') then
ProcessOfficialDocument(LFile);(Bear in mind negated alternation consumes a fixed-length slice equal to the longest alternative - it is not a true word-boundary check, just a positional negation.)
type
TWildCard = record
public
// Constructors - ACaseSensitive is locked in for the lifetime of the
// instance and defaults to case-insensitive (Windows convention).
class function Create(const ACaseSensitive: Boolean = False): TWildCard; overload; static;
class function Create(const APattern: string;
const ACaseSensitive: Boolean = False): TWildCard; overload; static;
class function Create(const APatterns: TArray<string>;
const ACaseSensitive: Boolean = False): TWildCard; overload; static;
class function Create(const APatterns: TStrings;
const ACaseSensitive: Boolean = False): TWildCard; overload; static;
// Match against the registered set only
function Match(const AInput: string): Boolean; overload;
// Match against an ad-hoc pattern; AAlsoMatchRegistered=True also
// tries the registered set after the ad-hoc one fails.
function Match(const AInput, APattern: string;
const AAlsoMatchRegistered: Boolean = False): Boolean; overload;
function Match(const AInput: string; const APatterns: TArray<string>;
const AAlsoMatchRegistered: Boolean = False): Boolean; overload;
function Match(const AInput: string; const APatterns: TStrings;
const AAlsoMatchRegistered: Boolean = False): Boolean; overload;
property CaseSensitive: Boolean read FCaseSensitive;
property RegisteredPatterns: TArray<string> read FPatterns;
end;The multi-pattern overloads return True on the first pattern that matches
and False on an empty pattern list. They do not report which pattern
matched - keep the call site simple.
Modern Delphi (records with methods, class function ... static, generics).
Tested on Delphi 12.x. No third-party dependencies.
DUnitX-based test suite under Unittests\. Open
Delphi.WildCardMatcher.Tests.dproj or run from the command line:
msbuild Unittests\Delphi.WildCardMatcher.Tests.dproj /t:Build /p:Config=Debug /p:Platform=Win32
Unittests\Win32\Debug\Delphi.WildCardMatcher.Tests.exe
See LICENSE.