Search code examples
inno-setuppascalscript

Inno Setup - Integer or Set/Range wildcard?


I need a wildcard that would match only numbers. I tried FileExistsWildcard function from How to test using wildcards whether a file exists in Inno Setup:

FileExistsWildcard(ExpandConstant('{app}\sav[1-9]'))

But Pascal Script obviously doesn't work that way. Is there such a wildcard or should I write a custom function or something?

P.S. Is there a wildcard matching list for Inno Setup at all?


Solution

  • The @TLama's FileExistsWildcard function internally uses Inno Setup FindFirst function, which in turn internally uses Windows FindFirstFile function.

    And Windows supports only * and ? in its wildcards. The range/set pattern [a-z] is *nix thing only.

    So it's not a Pascal (Script) limitation. It's a Windows limitation.


    Implementing a generic matching function that supports all of ?, * and [a-z] is not easy.

    I've tried to implement a matching function that is compatible with Windows matching (FindFirstFile) but supports a set pattern (including range set).

    I was not identify exact rules how Windows treat . in the mask and the filename. So my matching function does not behave exactly the same in that respect. Otherwise, I believe, it is identical. And it supports [abc] set pattern as well as range set pattern [a-z], or any combination like [_0-9a-z].

    function MatchesMaskEx(Mask: string; FileName: string): Boolean;
    var
      MaskI: Integer;
      MaskC: Char;
      FileNameI: Integer;
      FileNameI2: Integer;
      P: Integer;
      Mask2: string;
      EOSMatched: Boolean;
    begin
      Mask := LowerCase(Mask);
      FileName := LowerCase(FileName);
    
      MaskI := 1;
      FileNameI := 1;
      Result := True;
      EOSMatched := False;
    
      while (MaskI <= Length(Mask)) and Result do
      begin
        MaskC := Mask[MaskI];
    
        if MaskC = '?' then
        begin
          { noop, ? matches anything, even beyond end-of-string }
          Inc(FileNameI);
        end
          else
        if MaskC = '[' then
        begin
          if FileNameI > Length(FileName) then
          begin
            Result := False;
          end
            else
          begin
            P := Pos(']', Copy(Mask, MaskI + 1, Length(Mask) - MaskI));
    
            if  P = 0 then
            begin
              { unclosed set - no match }
              Result := False;
            end
              else
            begin
              Result := False;
              P := P + MaskI;
              Inc(MaskI);
              while (MaskI < P) and (not Result) do
              begin
                MaskC := Mask[MaskI];
                { is it range (A-Z) ? }
                if (MaskI + 2 < P) and (Mask[MaskI + 1] = '-') then
                begin
                  MaskI := MaskI + 2;
                end;
    
                { matching the range (or pseudo range A-A) }
                if (MaskC <= FileName[FileNameI]) and
                   (FileName[FileNameI] <= Mask[MaskI]) then
                begin
                  Inc(FileNameI);
                  Result := True;
                  MaskI := P - 1;
                end;
                Inc(MaskI);
              end;
            end;
          end;
        end
          else
        if MaskC = '*' then
        begin
          Mask2 := Copy(Mask, MaskI + 1, Length(Mask) - MaskI);
          Result := False;
          { Find if the rest of the mask can match any remaining part }
          { of the filename => recursion }
          for FileNameI2 := FileNameI to Length(FileName) + 1 do
          begin
            if MatchesMaskEx(
                 Mask2, Copy(FileName, FileNameI2, Length(FileName) - FileNameI2 + 1)) then
            begin
              Result := True;
              MaskI := Length(Mask);
              FileNameI := Length(FileName) + 1;
              break;
            end;
          end;
        end
          else
        begin
          if (FileNameI <= Length(FileName)) and (FileName[FileNameI] = MaskC) then
          begin
            Inc(FileNameI);
          end
            else
          begin
            { The dot can match EOS too, but only once }
            if (MaskC = '.') and (FileNameI > Length(FileName)) and (not EOSMatched) then
            begin
              EOSMatched := True;
            end
              else
            begin
              Result := False;
            end;
          end;
        end;
    
        Inc(MaskI);
      end;
    
      if Result and (FileNameI <= Length(FileName)) then
      begin
        Result := False;
      end;
    end;
    

    Use it like:

    function FileExistsEx(Path: string): Boolean;
    var
      FindRec: TFindRec;
      Mask: string;
    begin
      if FindFirst(AddBackslash(ExtractFilePath(Path)) + '*', FindRec) then
      begin
        Mask := ExtractFileName(Path);
        try
          repeat
            if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0) and
               MatchesMaskEx(Mask, FindRec.Name) then
            begin
              Result := True;
              Exit;
            end;
          until not FindNext(FindRec);
        finally
          FindClose(FindRec);
        end;
      end;
    
      Result := False;
    end;
    

    For your specific needs, you can also use a simple ad-hoc function like:

    function SpecialFileExists(Path: string): Boolean;
    var
      FindRec: TFindRec;
    begin
      if FindFirst(AddBackslash(Path) + '*', FindRec) then
      begin
        try
          repeat
            if (Length(FindRec.Name) = 4) and
               (Copy(FindRec.Name, 1, 3) = 'sav') and
               (FindRec.Name[4] >= '0') and (FindRec.Name[4] <= '9') then
            begin
              Result := True;
              Exit;
            end;
          until not FindNext(FindRec);
        finally
          FindClose(FindRec);
        end;
      end;
      
      Result := False;
    end;
    

    Use it like:

    SpecialFileExists(ExpandConstant('{app}'))