codetools: FilenameIsMatching: support for nested {}, multiple *, UTF-8 and special chars, issue #27393

git-svn-id: trunk@47711 -
This commit is contained in:
mattias 2015-02-11 23:18:42 +00:00
parent 91b63d9feb
commit e5885ee170
2 changed files with 228 additions and 202 deletions

View File

@ -1548,18 +1548,19 @@ begin
Result:=''; Result:='';
end; end;
function FilenameIsMatching(const Mask, Filename: string; function FilenameIsMatching(const Mask, Filename: string; MatchExactly: boolean
MatchExactly: boolean): boolean; ): boolean;
(* (*
check if Filename matches Mask check if Filename matches Mask
if MatchExactly then the complete Filename must match, else only the if MatchExactly then the complete Filename must match, else only the
start start
Filename matches exactly or is a file/directory in a subdirectory of mask Filename matches exactly or is a file/directory in a subdirectory of mask.
Mask can contain the wildcards * and ? and the set operator {,} Mask can contain the wildcards * and ? and the set operator {,}.
The wildcards will _not_ match PathDelim The wildcards will *not* match PathDelim.
You can nest the {} sets.
If you need the asterisk, the question mark or the PathDelim as character If you need the asterisk, the question mark or the PathDelim as character
just put the SpecialChar character in front of it. just put the SpecialChar character in front of it (e.g. #*, #? #/).
Examples: Examples:
/abc matches /abc, /abc/p, /abc/xyz/filename /abc matches /abc, /abc/p, /abc/xyz/filename
@ -1569,206 +1570,209 @@ function FilenameIsMatching(const Mask, Filename: string;
/abc/x*z/www matches /abc/xz/www, /abc/xyz/www, /abc/xAAAz/www /abc/x*z/www matches /abc/xz/www, /abc/xyz/www, /abc/xAAAz/www
but not /abc/x/z/www but not /abc/x/z/www
/abc/x#*z/www matches /abc/x*z/www, /abc/x*z/www/ttt /abc/x#*z/www matches /abc/x*z/www, /abc/x*z/www/ttt
/a{b,c,d}e matches /abe, /ace, /ade /a{b,c,d}e matches /abe, /ace, /ade
*.p{as,p,} matches a.pas unit1.pp b.p but not b.inc
*.{p{as,p,},inc} matches a.pas unit1.pp b.p b.inc but not c.lfm
*) *)
{off $DEFINE VerboseFilenameIsMatching}
function FindDirectoryStart(const AFilename: string; function Check(MaskP, FileP: PChar): boolean;
CurPos: integer): integer;
begin
Result:=CurPos;
while (Result<=length(AFilename))
and (AFilename[Result]=PathDelim) do
inc(Result);
end;
function FindDirectoryEnd(const AFilename: string; CurPos: integer): integer;
begin
Result:=CurPos;
while (Result<=length(AFilename)) do begin
if AFilename[Result]=SpecialChar then
inc(Result,2)
else if (AFilename[Result]=PathDelim) then
break
else
inc(Result);
end;
end;
function CharsEqual(c1, c2: char): boolean;
begin
{$ifdef CaseInsensitiveFilenames}
Result:=(FPUpChars[c1]=FPUpChars[c2]);
{$else}
Result:=(c1=c2);
{$endif}
end;
var var
DirStartMask, DirEndMask, Level: Integer;
DirStartFile, DirEndFile, MaskStart: PChar;
BracketMaskPos, BracketFilePos: integer; FileStart: PChar;
StopChar: LongInt;
Fits: Boolean;
function TryNextOr: boolean;
begin begin
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check Mask="',MaskP,'" FileP="',FileP,'"']);
{$ENDIF}
Result:=false; Result:=false;
if BracketMaskPos<1 then exit;
repeat repeat
inc(DirStartMask); case MaskP^ of
if DirStartMask>=DirEndMask then exit; // error, missing } #0:
if Mask[DirStartMask]=SpecialChar then begin begin
// special char -> next char is normal char // the whole Mask fits the start of Filename
inc(DirStartMask); // trailing PathDelim in FileP are ok
end else if Mask[DirStartMask]='}' then begin {$IFDEF VerboseFilenameIsMatching}
// bracket found (= end of Or operator) debugln([' Check END Mask="',MaskP,'" FileP="',FileP,'"']);
// -> filename does not match {$ENDIF}
if FileP^=#0 then exit(true);
if FileP^<>PathDelim then exit(false);
while FileP^=PathDelim do inc(FileP);
Result:=(FileP^=#0) or (not MatchExactly);
exit; exit;
end else if Mask[DirStartMask]=',' then begin
// next Or found
// -> reset filename position and compare
inc(DirStartMask);
DirStartFile:=BracketFilePos;
exit(true);
end; end;
until false; SpecialChar:
end;
begin begin
//debugln(['[FilenameIsMatching] Mask="',Mask,'" Filename="',Filename,'" MatchExactly=',MatchExactly]); // match on character
Result:=false; {$IFDEF VerboseFilenameIsMatching}
if (Filename='') then exit; debugln([' Check specialchar Mask="',MaskP,'" FileP="',FileP,'"']);
if (Mask='') then begin {$ENDIF}
Result:=true; exit; inc(MaskP);
if MaskP^=#0 then exit;
if MaskP^<>FileP^ then exit;
inc(MaskP);
inc(FileP);
end;
PathDelim:
begin
// match PathDelim(s) or end of filename
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check PathDelim Mask="',MaskP,'" FileP="',FileP,'"']);
{$ENDIF}
if not (FileP^ in [#0,PathDelim]) then exit;
// treat several PathDelim as one
while MaskP^=PathDelim do inc(MaskP);
while FileP^=PathDelim do inc(FileP);
end; end;
// test every directory
DirStartMask:=1;
DirStartFile:=1;
repeat
// find start of directories
DirStartMask:=FindDirectoryStart(Mask,DirStartMask);
DirStartFile:=FindDirectoryStart(Filename,DirStartFile);
// find ends of directories
DirEndMask:=FindDirectoryEnd(Mask,DirStartMask);
DirEndFile:=FindDirectoryEnd(Filename,DirStartFile);
//debugln(' Compare "',copy(Mask,DirStartMask,DirEndMask-DirStartMask),'"',
// ' "',copy(Filename,DirStartFile,DirEndFile-DirStartFile),'"');
// compare directories
BracketMaskPos:=0;
while (DirStartMask<DirEndMask) do begin
//debugln(['FilenameIsMatching ',DirStartMask,' ',Mask[DirStartMask],' - ',DirStartFile,' ',Pchar(Filename)[DirStartFile-1]]);
case Mask[DirStartMask] of
'?': '?':
if DirStartFile<DirEndFile then begin begin
inc(DirStartMask); // match any one character, but PathDelim
inc(DirStartFile); {$IFDEF VerboseFilenameIsMatching}
continue; debugln([' Check any one char Mask="',MaskP,'" FileP="',FileP,'"']);
end else begin {$ENDIF}
if not TryNextOr then exit; if FileP^ in [#0,PathDelim] then exit;
inc(MaskP);
inc(FileP,UTF8CharacterLength(FileP));
end; end;
'*': '*':
begin begin
inc(DirStartMask); // match 0 or more characters, but PathDelim
Fits:=false; {$IFDEF VerboseFilenameIsMatching}
if (DirStartMask=DirEndMask) then begin debugln([' Check any chars Mask="',MaskP,'" FileP="',FileP,'"']);
Fits:=true; {$ENDIF}
end else begin while MaskP^='*' do inc(MaskP);
StopChar:=DirStartMask; repeat
if (BracketMaskPos>0) and (Mask[StopChar] in [',','}']) then begin if Check(MaskP,FileP) then exit(true);
while (StopChar<DirEndMask) and (Mask[StopChar]<>'}') do if FileP^ in [#0,PathDelim] then exit;
inc(StopChar); inc(FileP);
inc(StopChar); until false;
end;
if StopChar>=DirEndMask then
Fits:=true
else begin
while (DirStartFile<DirEndFile)
and (not CharsEqual(Filename[DirStartFile],Mask[StopChar]))
do
inc(DirStartFile);
Fits:=DirStartFile<DirEndFile;
end;
end;
if (not Fits) and (not TryNextOr) then exit;
continue;
end; end;
'{': '{':
if BracketMaskPos<1 then begin begin
inc(DirStartMask); // OR options separated by comma
BracketMaskPos:=DirStartMask; {$IFDEF VerboseFilenameIsMatching}
BracketFilePos:=DirStartFile; debugln([' Check { Mask="',MaskP,'" FileP="',FileP,'"']);
continue; {$ENDIF}
end else begin inc(MaskP);
// treat as normal character repeat
if Check(MaskP,FileP) then begin
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check { option fits -> end']);
{$ENDIF}
exit(true);
end;
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check { skip to next option ...']);
{$ENDIF}
// skip to next option in MaskP
Level:=1;
repeat
case MaskP^ of
#0: exit;
SpecialChar:
begin
inc(MaskP);
if MaskP^=#0 then exit;
inc(MaskP);
end;
'{': inc(Level);
'}':
begin
dec(Level);
if Level=0 then exit; // no option fits
end; end;
',': ',':
if BracketMaskPos>0 then begin if Level=1 then break;
// Bracket operator fits complete
// -> skip rest of Bracket operator
repeat
inc(DirStartMask);
if DirStartMask>=DirEndMask then exit; // error, missing }
if Mask[DirStartMask]=SpecialChar then begin
// special char -> next char is normal char
inc(DirStartMask);
end else if Mask[DirStartMask]='}' then begin
// bracket found (= end of Or operator)
inc(DirStartMask);
break;
end; end;
inc(MaskP);
until false;
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check { next option: "',MaskP,'"']);
{$ENDIF}
inc(MaskP)
until false; until false;
BracketMaskPos:=0;
continue;
end else begin
// treat as normal character
end; end;
'}': '}':
if BracketMaskPos>0 then begin begin
// Bracket operator fits complete {$IFDEF VerboseFilenameIsMatching}
inc(DirStartMask); debugln([' Check } Mask="',MaskP,'" FileP="',FileP,'"']);
BracketMaskPos:=0; {$ENDIF}
continue; inc(MaskP);
end else begin end;
// treat as normal character ',':
begin
// OR option fits => continue behind the {}
{$IFDEF VerboseFilenameIsMatching}
debugln([' Check Skipping to end of {} Mask="',MaskP,'" ...']);
{$ENDIF}
Level:=1;
repeat
inc(MaskP);
case MaskP^ of
#0: exit;
SpecialChar:
begin
inc(MaskP);
if MaskP^=#0 then exit;
inc(MaskP);
end;
'{': inc(Level);
'}':
begin
dec(Level);
if Level=0 then break;
end; end;
end; end;
if Mask[DirStartMask]=SpecialChar then begin until false;
// special char -> next char is normal char {$IFDEF VerboseFilenameIsMatching}
inc(DirStartMask); debugln([' Check Skipped to end of {} Mask="',MaskP,'"']);
if (DirStartMask>=DirEndMask) then exit; {$ENDIF}
inc(MaskP);
end; end;
// compare char #128..#255:
if (DirStartFile<DirEndFile) begin
and CharsEqual(Mask[DirStartMask],Filename[DirStartFile]) then begin // match UTF-8 characters
inc(DirStartMask); {$IFDEF VerboseFilenameIsMatching}
inc(DirStartFile); debugln([' Check UTF-8 chars Mask="',MaskP,'" FileP="',FileP,'"']);
end else begin {$ENDIF}
// chars different MaskStart:=MaskP;
if (BracketMaskPos=0) or (not TryNextOr) then begin FileStart:=FileP;
// filename does not match while not (MaskP^ in [#0,SpecialChar,PathDelim,'?','*','{',',','}']) do
begin
if FileP^ in [#0,PathDelim] then exit;
inc(MaskP,UTF8CharacterLength(MaskP));
inc(FileP,UTF8CharacterLength(FileP));
end;
if CompareFilenames(MaskStart,MaskP-MaskStart,FileStart,FileP-FileStart)<>0 then
exit; exit;
end; end;
else
// match ASCII characters
repeat
case MaskP^ of
#0,SpecialChar,PathDelim,'?','*','{',',','}': break;
{$IFDEF CaseInsensitiveFilenames}
'a'..'z','A'..'Z':
if FPUpChars[MaskP^]<>FPUpChars[FileP^] then exit;
{$ENDIF}
else
if MaskP^<>FileP^ then exit;
end; end;
inc(MaskP);
inc(FileP);
until false;
end; end;
if BracketMaskPos>0 then exit; until false;
if (DirStartMask<DirEndmask) or (DirStartFile<DirEndFile) then exit;
// find starts of next directories
DirStartMask:=DirEndMask+1;
DirStartFile:=DirEndFile+1;
until (DirStartFile>length(Filename)) or (DirStartMask>length(Mask));
DirStartMask:=FindDirectoryStart(Mask,DirStartMask);
// check that complete mask matches
Result:=(DirStartMask>length(Mask));
if MatchExactly then begin
DirStartFile:=FindDirectoryStart(Filename,DirStartFile);
// check that the complete Filename matches
Result:=(Result and (DirStartFile>length(Filename)));
end; end;
//debugl(' [FilenameIsMatching] Result=',Result,' ',DirStartMask,',',length(Mask),' ',DirStartFile,',',length(Filename));
begin
if Filename='' then exit(false);
if Mask='' then exit(true);
{$IFDEF VerboseFilenameIsMatching}
debugln(['FilenameIsMatching2 Mask="',Mask,'" File="',Filename,'" Exactly=',MatchExactly]);
{$ENDIF}
Result:=Check(PChar(Mask),PChar(Filename));
end; end;
function FindNextDirectoryInFilename(const Filename: string; function FindNextDirectoryInFilename(const Filename: string;

View File

@ -331,6 +331,28 @@ begin
t('/a{b,c,d}e','/ade',false,true); t('/a{b,c,d}e','/ade',false,true);
t('/a{b,c,d}e','/aze',true,false); t('/a{b,c,d}e','/aze',true,false);
t('/a{b,c,d}e','/aze',false,false); t('/a{b,c,d}e','/aze',false,false);
// {*.pas,*.pp,*.p,*.inc,Makefile.fpc} matches math.pp
t('{*.pas,*.pp,*.p,*.inc,Makefile.fpc}','math.pp',true,true);
t('{*.pas}','Generics.Collections.pas',true,true);
t('{*.pas,*.pp}','Generics.Collections.pas',true,true);
t('{*.pas,*.pp,*.p}','Generics.Collections.pas',true,true);
t('{*.pas,*.pp,*.p,*.inc}','Generics.Collections.pas',true,true);
t('{*.pas,*.pp,*.p,*.inc,Makefile.fpc}','Generics.Collections.pas',true,true);
// *{.p{as,p,}} matches a.pas unit1.pp b.p but not b.inc
t('*{.p{as,p,}}','a.pas',true,true);
t('*{.p{as,p,}}','unit1.pp',true,true);
t('*{.p{as,p,}}','b.p',true,true);
t('*{.p{as,p,}}','b.inc',true,false);
// *.{p{as,p,},inc} matches a.pas unit1.pp b.p b.inc but not c.lfm
t('*.{p{as,p,},inc}','a.pas',true,true);
t('*.{p{as,p,},inc}','unit1.pp',true,true);
t('*.{p{as,p,},inc}','b.p',true,true);
t('*.{p{as,p,},inc}','b.inc',true,true);
t('*.{p{as,p,},inc}','c.lfm',true,false);
end; end;
initialization initialization