Lazarus: StringReplace ineffective when working with files (unicode issue) - string

I'm using Lazarus to build a simple app that builds Outlook signatures based on a template. The idea is to extract the template (a ZIP file), and replace variables within the files it contains.
For example, I may want to replace {fullname} with the name provided by the user.
I am currently using the implementation below, but it seems to be ineffective. The file is read and written to, but it appears the replacements are not being made. I have tested to see if my implementation of TFileStream is not correct, but using WriteAnsiString to append dummy text onto the end of the output file works.
Please would you kindly have a look at my code below and let me know what I may have done wrong, or if there are any better alternatives to StringReplace? I am aware that one can use TStringList - however, doing so breaks line endings. As memos and rich edits use TStringList, using those won't help either.
Update:
I have seen this, but using AnsiString makes no difference. If I'm not mistaken, FPC uses it by default anyway, instead of UnicodeString.
Update 2:
Indeed, AnsiString is the default. Using a unicode string (which makes the replacements work) adds ? to the beginning and end of the file. Why would it do that?
function multiStringReplace(const s: string; search, replace : array of string; flags : tReplaceFlags): string;
var c : cardinal;
begin
assert(length(search) = length(replace), 'Array lengths differ.');
result := s;
for c := low(search) to high(search) do
result := stringReplace(result, search[c], replace[c], flags);
end;
procedure fileReplaceString(const fileName: string; search, replace: array of string);
var
fs: tFileStream;
s: string;
begin
fs := tFileStream.create(fileName, fmOpenRead or fmShareDenyNone);
try
setLength(s, fs.size);
fs.readBuffer(s[1], fs.size);
finally
fs.free();
end;
s := multiStringReplace(s, search, replace, [rfReplaceAll, rfIgnoreCase]);
fs := tFileStream.create(fileName, fmOpenWrite);
try
fs.writeBuffer(s[1], length(s));
finally
fs.free();
end;
end;
Usage:
fileReplaceString(currentFile, ['{fullname}'], ['Full Name']);

Thanks to Abelisto's comment above, it appears the issue is due to the fact that Outlook saves the three files it creates with different encodings. To get around it, I simply used convertEncoding and guessEncoding from lconvencoding, as below:
uses
lconvencoding;
// Read string
s := convertEncoding(
multiStringReplace(s, search, replace, [rfReplaceAll, rfIgnoreCase]),
guessEncoding(s), encodingAnsi
);
// Write modified and converted string back to file
encodingAnsi appears to be the best conversion, at least in my case. Converting to UTF8 (with or without BOM) caused a bit of a headache with certain characters, specifically EmDash or EnDash.

Related

Array variables and dynamic access in [Code] section

My installer has Components which come associated with downloadable files. These things are changing from build to build, so I'm using #insert to create the [Components] section as well as the appropriate entries in the [Files] section.
Some of these components rely on common downloadable files.
To now include the correct urls in the downloads page, I'm currently defining array variables that are named like the component and have as values the names of the required downloadable files, for example:
#dim myfeature[2] {"01aed27862e2087bd117e9b677a8685aebb0be09744723b4a948ba78d6011bac", "677756ac5969f814fd01ae677dbb832ab2e642513fea44ea0a529430d5ec1fdc"}
In the code for the download page I'm checking which components where selected via WizardSelectedComponents() and after converting the string to an array of strings, I'm trying to get to the previously defined variable and that is where I'm failing:
function GetDownloads(): Array of String;
var
Downloads: Array of String;
SelectedComponents: String;
SelectedArray: Array of String;
begin
SelectedComponents := WizardSelectedComponents(False);
// a custom procedure to parse the comma seperated string
SelectedArray := ParseArray(SelectedComponents, SelectedArray);
// trying to get to the constant array now this works:
MsgBox(ExpandConstant('{#myfeature[0]}'), mbInformation, MB_OK);
// same but trying to use the selected component value returns this as a literal
// '+SelectedArray[0]+' instead the expanded value
MsgBox(ExpandConstant('{#' + SelectedArray[0] + '[0]}'), mbInformation, MB_OK);
end;
So I understand something is up with the # mark but I could not find a way to solve this properly.
Thank you!
Markus
ExpandConstant expands Inno Setup "constants", not preprocessor values. See also Evaluate preprocessor macro on run time in Inno Setup Pascal Script.
You cannot access elements of a preprocessor compile-time array using run-time indexes.
If you know C/C++, it's like if you were trying to do:
#define VALUE1 123
#define VALUE2 456
int index = 1;
int value = VALUE ## index
I'm not really sure I completely understand what are you doing. But it seems that you need to create an array on compile time from various sources and use it on runtime.
There are several approaches that can be used for that. But you definitely need runtime array initialized on run time. But the code that initializes it can be generated on compile time.
An example of the approach follows (and some links to other approaches are at the end).
At the beginning of your script, define these support functions:
[Code]
var
FeatureDownloads: TStrings;
function AddFeature(
Feature: Integer; CommaSeparatedListOfDownloads: string): Boolean;
begin
if not Assigned(FeatureDownloads) then
begin
FeatureDownloads := TStringList.Create();
end;
while FeatureDownloads.Count <= Feature do
FeatureDownloads.Add('');
if FeatureDownloads[Feature] <> '' then
RaiseException('Downloads for feature already defined');
FeatureDownloads[Feature] := CommaSeparatedListOfDownloads;
Result := True;
end;
#define AddFeature(Feature, CommaSeparatedListOfDownloads) \
"<event('InitializeSetup')>" + NewLine + \
"function InitializeSetupFeature" + Str(Feature) + "(): Boolean;" + NewLine + \
"begin" + NewLine + \
" Result := AddFeature(" + Str(Feature) + ", '" + CommaSeparatedListOfDownloads + "');" + NewLine + \
"end;"
In your components include files, do:
#emit AddFeature(2, "01aed27862e2087bd117e9b677a8685aebb0be09744723b4a948ba78d6011bac,677756ac5969f814fd01ae677dbb832ab2e642513fea44ea0a529430d5ec1fdc")
If you add:
#expr SaveToFile(AddBackslash(SourcePath) + "Preprocessed.iss")
to the end of your main script, you will see in the Preprocessed.iss generated by the preprocessor/compiler that the #emit directive expands to:
<event('InitializeSetup')>
function InitializeSetupFeature2(): Boolean;
begin
Result := AddFeature(2, '01aed27862e2087bd117e9b677a8685aebb0be09744723b4a948ba78d6011bac,677756ac5969f814fd01ae677dbb832ab2e642513fea44ea0a529430d5ec1fdc');
end;
Now you have FeatureDownloads Pascal Script runtime variable that you can access using FeatureDownloads[SelectedArray[0]] to get comma-separated string, which you can parse to the individual downloads.
This can be optimimized/improved a lot, but I do not know/understand the extent of your task. But I believe that once you grasp the concept (it might be difficult at the beginning), you will be able to do it yourself.
Another similar questions:
Evaluate a collection of data from preprocessor on run time in Inno Setup Pascal Script (simple example that be easier to grasp initially)
Scripting capabilities in the Registry section (slightly different approach from times event attributes were not available yet – and that's YOUR question)

Finding the line number of a specific string in a text file using Delphi

I want to implement a Delphi function which returns the line number and occurrence of a specific string inside a text file. say in the given text file one of the line is: #1.1 Torsional Stiffness [Tz] along with other text data.
If I want to find out where Torsional Stiffness is seated in the text file, what Delphi code should be implemented??
The below program works and helps me find a particular string inside the text file.
The string is visually found on line number 4 (sl[i] = sl[4]) of the assigned text file (using RAD debug layout). But I want my program to automatically output the line where my string is located and also count the number of occurrences of the same in the .txt file.
program findingText;
uses
SysUtils,
Dialogs,
Classes;
procedure FindText;
var
sl: TStringList;
i: Integer;
searchText: string;
//fileName: TextFile;
begin
searchText := 'Torsional Stiffness matrices';
sl := TStringList.Create;
try
sl.LoadFromFile('c:\Users\fro.txt');
for i := 0 to sl.Count-1 do
if Pos(searchText, sl[i])>0 then
begin
ShowMessage('Yes');
//ShowMessage(sl[i]);
Break;
end;
finally
sl.Free;
end;
end;
begin
FindText;
//readln;
end.
As with any problem, there will many possible approaches. I don't have a particular solution but I would start by loading the text into a string list. TStringList has a LoadFromFile method and the ItemIndex property will then provide the line number. You would still need to work out the best method to search for a given string. A for/next loop iterating through the items in the string list would be easiest. If the file is relatively small then this approach is very simple and effective. You'd need to experiment as to how large the file could go before speed became an issue.

SaveTo StringHelper?

I need a StringHelper which saves a string to a file:
var
s: string;
begin
s := 'Some text';
s.SaveTo('C:\MyText.txt');
end;
Unfortunately, this is not possible. Is it possible to add such a StringHelper?
It is possible to add such a helper. For instance:
type
TMyStringHelper = record helper for string
procedure SaveTo(const FileName: string);
end;
The downside to doing so is that this will replace the string helper that is provided by the RTL. If you don't use it, that won't matter. If you do use it, then that's a problem that cannot readily be overcome.
You could look at this a different way. Instead of trying to use a helper on the string type, you could use TFile.WriteAllText instead.
TFile.WriteAllText(FileName, 'Some text', TEncoding.UTF8);
Obviously you can use a different encoding if you prefer.

How to write String format of binary value to registry using Delphi and writebinary command?

Fore some good reason i need to write a string format of a binary value to a binary registry key, in other words I have a registry value like this :
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Sonal]
"Password"=hex:00,d6
I tried to write it to registry using following code :
procedure Tesct.Button2Click(Sender: TObject);
var
RegInfoExists : TRegistry;
EdStr : AnsiString;
begin
try
RegInfoExists := TRegistry.Create(KEY_WRITE OR KEY_WOW64_64KEY);
RegInfoExists.RootKey := HKEY_LOCAL_MACHINE;
if RegInfoExists.OpenKey('SOFTWARE\Sonal',true) then
EdStr := #$00#$d6;
RegInfoExists.WriteBinaryData('Password', EdStr,
Length(EdStr) * SizeOf(byte));
except
end;
RegInfoExists.CloseKey;
RegInfoExists.Free;
end;
And I got this :
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Sonal]
"Password"=hex:dc,d7
How i can do this ?
You are writing the value of the string variable which is the address of the string. That's because at an implementation level, strings are simply pointers.
Instead you need to de-reference the pointer. Two commonly used ways to do that:
RegInfoExists.WriteBinaryData('Password', EdStr[1],
Length(EdStr)*SizeOf(AnsiChar));
or
RegInfoExists.WriteBinaryData('Password', Pointer(EdStr)^,
Length(EdStr)*SizeOf(AnsiChar));
If you really are just trying to write those two bytes then it is easier like this:
const
Password: array [0..1] of byte = ($00, $d6);
....
RegInfoExists.WriteBinaryData('Password', Password, Length(Password));
I would also comment that you should be very strict and precise in the way you look after resources. That means using try/finally correctly. Like this:
SomeInstance := TSomeClass.Create;
try
.... do stuff with SomeInstance
finally
SomeInstance.Free;
end;
And you also seem to have omitted the begin/end that you need for the code that follows the if statement.

How to save a string to a .txt file in Delphi?

I need to make a program that generates a password that is saved in a text file format in a specific destination I set and the user needs to open the .txt to get the password to 'unlock' another program.
I have already got the code to generate the password in the string sPass and now I need to use the SaveToFile function to save it into the text file I created called Password.txt but I cannot find the general form to use the SaveTo File Function in Delphi and I do not know where to put the sPass and Password.txt in the function.
It should be something like : SaveToFile(...) but I do not know how to save sPass in Password.txt
Edit :
Just one more question, how do you delete what is previously stored in Password.txt before you add the string to it so that Password.txt is blank before the string is added ? Thanks
The Modern Modern way is to use TFile.WriteAllText in IOUtils (Delphi 2010 and up)
procedure WriteAllText(const Path: string; const Contents: string);
overload; static;
Creates a new file, writes the specified string to the file, and then
closes the file. If the target file already exists, it is overwritten.
The modern way is to create a stringlist and save that to file.
procedure MakeAStringlistAndSaveThat;
var
MyText: TStringlist;
begin
MyText:= TStringlist.create;
try
MyText.Add('line 1');
MyText.Add('line 2');
MyText.SaveToFile('c:\folder\filename.txt');
finally
MyText.Free
end; {try}
end;
Note that Delphi already has a related class that does everything you want: TInifile.
It stores values and keys in a key = 'value' format.
passwordlist:= TInifile.Create;
try
passwordlist.LoadFromFile('c:\folder\passwords.txt');
//Add or replace a password for `user1`
passwordlist.WriteString('sectionname','user1','topsecretpassword');
passwordlist.SaveToFile('c:\folder\passwords.txt');
finally
passwordlist.Free;
end; {try}
Warning
Note that saving unecrypted passwords in a textfile is a security-leak. It's better to hash your passwords using a hashfunction, see: Password encryption in Delphi
For tips on how to save passwords in a secure way.
You can use the TFileStream class to save a string to a file:
uses
Classes;
procedure StrToFile(const FileName, SourceString : string);
var
Stream : TFileStream;
begin
Stream:= TFileStream.Create(FileName, fmCreate);
try
Stream.WriteBuffer(Pointer(SourceString)^, Length(SourceString));
finally
Stream.Free;
end;
end;
and to read
function FileToStr(const FileName : string):string;
var
Stream : TFileStream;
begin
Stream:= TFileStream.Create(FileName, fmOpenRead);
try
SetLength(Result, Stream.Size);
Stream.Position:=0;
Stream.ReadBuffer(Pointer(Result)^, Stream.Size);
finally
Stream.Free;
end;
end;
Fastest and simplest way, no need to declare any variables:
with TStringList.Create do
try
Add(SomeString);
SaveToFile('c:\1.txt');
finally
Free;
end;

Resources