Skip to content

Instantly share code, notes, and snippets.

@mmmunk
Created October 28, 2020 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmmunk/50bbe590c43cfd84898b41f24b0294a0 to your computer and use it in GitHub Desktop.
Save mmmunk/50bbe590c43cfd84898b41f24b0294a0 to your computer and use it in GitHub Desktop.
Search for and optionally replace strings in files
{ BinSearchReplace Version 0.9 }
program BinSearchReplace;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows, SysUtils;
const
cFileChunkSize = 500000;
var
ArgFileName: string = '';
ArgSearchStr: string = '';
ArgReplaceStr: string = '';
ArgAnsi: Boolean = False;
ArgUtf8: Boolean = False;
ArgUtf16: Boolean = False;
ArgNoCase: Boolean = False;
ArgZero: Boolean = False;
ArgBackup: Boolean = False;
FileChunk: packed array[0..cFileChunkSize-1] of Byte;
SearchData, SearchData2, ReplaceData: packed array[0..4096-1] of Byte;
DataSize: Integer;
ReplaceDataSize: Cardinal = 0;
Replace: Boolean = False;
ReplacePos: array[0..2048-1] of UInt64;
ReplacePosCount: Integer;
procedure Usage;
var
S: string;
i: Integer;
begin
S:=ExtractFileName(ParamStr(0));
WriteLn;
WriteLn('Usage: ', S, ' [-Ansi] [-Utf8] [-Utf16] [-Backup] [-NoCase] [-Zero]');
for i:=1 to Length(S) do S[i]:=' ';
WriteLn(S, ' FileName SearchStr [ReplaceStr]');
WriteLn;
WriteLn('Search for and optionally replace strings in files');
WriteLn;
WriteLn('Positional arguments:');
WriteLn(' FileName Relative or full path to file or wildcards of multiple files');
WriteLn(' SearchStr String to find all instances of in FileName');
WriteLn(' ReplaceStr String to insert in file instead of each SearchStr instance');
WriteLn(' Must be same length as SearchStr');
WriteLn;
WriteLn('String conversion arguments:');
WriteLn('(SearchStr and ReplaceStr will be converted to this format before use)');
WriteLn(' -Ansi (default)');
WriteLn(' -Utf8');
WriteLn(' -Utf16');
WriteLn;
WriteLn('Other arguments:');
WriteLn(' -NoCase String search is case insensitive');
WriteLn(' -Zero Expect a zero termination after SearchStr');
WriteLn(' -Backup Copy file.ext to file.ext.backup before replacing strings');
end;
function ParseAndValidateArguments: Boolean;
var
i, P, L1, L2: Integer;
Arg: string;
S, S2: string;
RS, RS2: RawByteString;
begin
Result:=False;
P:=1;
for i:=1 to ParamCount do
begin
Arg:=ParamStr(i);
if AnsiChar(Arg[1]) in ['-', '/'] then
begin
Delete(Arg, 1, 1);
if SameText(Arg, 'ansi') then
ArgAnsi:=True
else
if SameText(Arg, 'utf8') then
ArgUtf8:=True
else
if SameText(Arg, 'utf16') then
ArgUtf16:=True
else
if SameText(Arg, 'backup') then
ArgBackup:=True
else
if SameText(Arg, 'nocase') then
ArgNoCase:=True
else
if SameText(Arg, 'zero') then
ArgZero:=True
else
begin
WriteLn('Unknown argument: ', Arg);
Exit;
end;
end
else
case P of
1:
begin
ArgFileName:=Arg;
Inc(P);
end;
2:
begin
ArgSearchStr:=Arg;
Inc(P);
end;
3:
begin
ArgReplaceStr:=Arg;
Inc(P);
end;
else
WriteLn('Too many positional arguments');
Exit;
end;
end;
if (ArgFileName = '') or (ArgSearchStr = '') then
begin
WriteLn('Both FileName and SearchStr arguments must be given');
Exit;
end;
L1:=Length(ArgSearchStr);
L2:=Length(ArgReplaceStr);
if L1 < 2 then
begin
WriteLn('SearchStr too short');
Exit;
end;
if (L1 > 1000) or (L2 > 1000) then
begin
WriteLn('SearchStr or ReplaceStr too long');
Exit;
end;
Replace:=L2 >= 1;
if Replace and ((L1 <> L2) or (ArgReplaceStr = ArgSearchStr)) then
begin
WriteLn('SearchStr and ReplaceStr must be different and of same length');
Exit;
end;
FillChar(SearchData, SizeOf(SearchData), 0);
FillChar(SearchData, SizeOf(SearchData2), 0);
if ArgUtf16 then
begin
if ArgNoCase then
begin
WriteLn('UTF-16 case insensitive');
S:=AnsiLowerCase(ArgSearchStr);
S2:=AnsiUpperCase(ArgSearchStr);
DataSize:=Length(S)*SizeOf(Char);
Assert(DataSize = Length(S2)*SizeOf(Char));
Move(S[1], SearchData[0], DataSize);
Move(S2[1], SearchData2[0], DataSize);
end
else
begin
WriteLn('UTF-16 case sensitive');
DataSize:=Length(ArgSearchStr)*SizeOf(Char);
Move(ArgSearchStr[1], SearchData[0], DataSize);
end;
Assert(DataSize = L1*SizeOf(Char));
if Replace then
begin
Assert(DataSize = L2*SizeOf(Char)); //TODO: Denne giver vel sig selv da L1=L2 ?
Move(ArgReplaceStr[1], ReplaceData[0], DataSize)
end;
end
else
if ArgUtf8 then
begin
if ArgNoCase then
begin
WriteLn('UTF-8 case insensitive');
RS:=Utf8Encode(AnsiLowerCase(ArgSearchStr));
RS2:=Utf8Encode(AnsiUpperCase(ArgSearchStr));
DataSize:=Length(RS);
Assert(DataSize = Length(RS2));
Move(RS[1], SearchData[0], DataSize);
Move(RS2[1], SearchData2[0], DataSize);
end
else
begin
WriteLn('UTF-8 case sensitive');
RS:=Utf8Encode(ArgSearchStr);
DataSize:=Length(RS);
Move(RS[1], SearchData[0], DataSize);
end;
if Replace then
begin
RS:=Utf8Encode(ArgReplaceStr);
Assert(DataSize = Length(RS)); //TODO: Dette er ret sandsynligt - skriv pæn meddelelse og exit
Move(RS[1], ReplaceData[0], DataSize);
end;
end
else
begin
if ArgNoCase then
begin
WriteLn('ANSI case insensitive');
RS:=AnsiString(AnsiLowerCase(ArgSearchStr));
RS2:=AnsiString(AnsiUpperCase(ArgSearchStr));
DataSize:=Length(RS);
Assert(DataSize = Length(RS2));
Move(RS[1], SearchData[0], DataSize);
Move(RS2[1], SearchData2[0], DataSize);
end
else
begin
WriteLn('ANSI case sensitive');
RS:=AnsiString(ArgSearchStr);
DataSize:=Length(RS);
Move(RS[1], SearchData[0], DataSize);
end;
Assert(DataSize = L1);
if Replace then
begin
RS:=AnsiString(ArgReplaceStr);
Assert(DataSize = Length(RS));
Move(RS[1], ReplaceData[0], DataSize);
end;
end;
if Replace then ReplaceDataSize:=DataSize;
if ArgZero then
begin
WriteLn('Zero-termination expected');
if ArgUtf16 then Inc(DataSize, 2) else Inc(DataSize, 1);
end;
Result:=True;
end;
procedure SearchAndReplace(const FileName: string);
var
FileHandle: THandle;
FilePos, N64: UInt64;
ScanSize, MoveSize, LastPos, i, j: Integer;
BytesRead: Cardinal;
label
Loop, EndLoop;
begin
WriteLn(FileName);
{ Search }
FileHandle:=CreateFile(PChar(FileName), GENERIC_READ, 0, nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
if FileHandle = INVALID_HANDLE_VALUE then
begin
WriteLn(#9'Error opening file: ', SysErrorMessage(GetLastError));
Exit;
end;
ReplacePosCount:=0;
FilePos:=0;
MoveSize:=0;
Loop:
if ReadFile(FileHandle, FileChunk[MoveSize], cFileChunkSize-MoveSize, BytesRead, nil) then
begin
if BytesRead = 0 then goto EndLoop;
ScanSize:=Integer(BytesRead)+MoveSize;
LastPos:=ScanSize-DataSize;
//DEBUG: writeln('filepos: ', filepos, ' scansize: ', scansize, ' movesize: ', movesize, ' lastpos: ', lastpos);
i:=0;
while i <= LastPos do
begin
j:=0;
while (j < DataSize) and ((FileChunk[i+j] = SearchData[j]) or (ArgNoCase and (FileChunk[i+j] = SearchData2[j]))) do Inc(j);
if j <> DataSize then
Inc(i)
else
begin
N64:=FilePos-MoveSize+i;
Inc(i, j);
if ReplacePosCount = 0 then WriteLn(#9'String found at file position:');
WriteLn(#9#9, IntToHex(N64, 0));
ReplacePos[ReplacePosCount]:=N64;
Inc(ReplacePosCount);
if ReplacePosCount = Length(ReplacePos) then
begin
WriteLn(#9'Maximum replacements reached');
goto EndLoop;
end;
end
end;
MoveSize:=ScanSize-i;
Move(FileChunk[i], FileChunk[0], MoveSize);
Inc(FilePos, BytesRead);
end
else
begin
ReplacePosCount:=0;
WriteLn(#9'Error reading file: ', SysErrorMessage(GetLastError));
goto EndLoop;
end;
goto Loop;
EndLoop:
if not CloseHandle(FileHandle) then Exit;
if (ReplacePosCount = 0) or (not Replace) then Exit;
{ Replace }
if ArgBackup then
if Windows.CopyFile(PChar(FileName), PChar(FileName+'.backup'), True) then
WriteLn(#9'Backup file created')
else
begin
WriteLn(#9'File backup error: ', SysErrorMessage(GetLastError));
Exit;
end;
FileHandle:=CreateFile(PChar(FileName), GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
if FileHandle = INVALID_HANDLE_VALUE then
begin
WriteLn(#9'Error opening file for writing: ', SysErrorMessage(GetLastError));
Exit;
end;
j:=0;
for i:=0 to ReplacePosCount-1 do
begin
N64:=0;
if not SetFilePointerEx(FileHandle, ReplacePos[i], @N64, FILE_BEGIN) then
begin
WriteLn(#9'Error setting file position: ', SysErrorMessage(GetLastError));
Break;
end;
if N64 <> ReplacePos[i] then
begin
WriteLn(#9'File not positioned as expected');
Break;
end;
BytesRead:=0;
if not WriteFile(FileHandle, ReplaceData[0], ReplaceDataSize, BytesRead, nil) then
begin
WriteLn(#9'Error writing file: ', SysErrorMessage(GetLastError));
Break;
end;
if BytesRead <> ReplaceDataSize then
begin
WriteLn(#9'Bytes not written as expected');
Break;
end;
Inc(j);
end;
CloseHandle(FileHandle);
WriteLn(#9'Strings replaced ', j, ':', ReplacePosCount);
end;
function ProcessFiles: Boolean;
var
FindHandle: THandle;
FindData: TWin32FindData;
Path: string;
begin
WriteLn('Scanning ', ArgFileName);
WriteLn;
Path:=ExtractFilePath(ArgFileName);
FindHandle:=FindFirstFile(PChar(ArgFileName), FindData);
if FindHandle = INVALID_HANDLE_VALUE then
begin
WriteLn(SysErrorMessage(GetLastError));
Exit(False);
end;
repeat
if FindData.dwFileAttributes and (
FILE_ATTRIBUTE_DIRECTORY or
FILE_ATTRIBUTE_HIDDEN or
FILE_ATTRIBUTE_DEVICE or
FILE_ATTRIBUTE_REPARSE_POINT or
FILE_ATTRIBUTE_SYSTEM or
FILE_ATTRIBUTE_TEMPORARY) = 0 then SearchAndReplace(Path+string(FindData.cFileName));
until not FindNextFile(Findhandle, FindData);
if GetLastError <> ERROR_NO_MORE_FILES then WriteLn(SysErrorMessage(GetLastError));
Windows.FindClose(FindHandle);
Result:=True;
end;
begin
Assert(SizeOf(Char) = 2);
if not ParseAndValidateArguments then
begin
Usage;
Halt(1);
end;
if not ProcessFiles then Halt(2);
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment