Created
October 28, 2020 14:36
-
-
Save mmmunk/50bbe590c43cfd84898b41f24b0294a0 to your computer and use it in GitHub Desktop.
Search for and optionally replace strings in files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ 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