Read strings from file and give option to choose installation - inno-setup

I want to make Steam backups installer. However, Steam allow to user make multiple library folders which makes installation difficult.
There are a few tasks that I want to perform.
Installer should identify path from registry to identify where Steam is installed.
The resulting path from registry open file
X:\Steam\config\config.vdf
and read value of "BaseInstallFolder_1", "BaseInstallFolder_2", "BaseInstallFolder_3" and etc.
Example of config.vdf:
"NoSavePersonalInfo" "0"
"MaxServerBrowserPingsPerMin" "0"
"DownloadThrottleKbps" "0"
"AllowDownloadsDuringGameplay" "0"
"StreamingThrottleEnabled" "1"
"AutoUpdateWindowStart" "-1"
"AutoUpdateWindowEnd" "-1"
"LastConfigstoreUploadTime" "1461497849"
"BaseInstallFolder_1" "E:\\Steam_GAMES"
The resulting path or paths of the file config.vdf bring in DirEdit
If user have multiple paths to the folder in different locations then give option select through DirTreeView or Radiobuttons.
How it should look like:
I know how to identify Steam path
WizardForm.DirEdit.Text := ExpandConstant('{reg:HKLM\SOFTWARE\Valve\Steam,InstallPath|{pf}\Steam}')+ '\steamapps\common\gamename';
But it is difficult to perform other tasks
Thanks in advance for your help.

To parse the config.vdf file:
Load the file using LoadStringsFromFile.
And use string functions like Copy, Pos, Delete, CompareText to parse it.
To keep the results use array of string (or predefined TArrayOfString). To allocate the array, use SetArrayLength.
The code may be like:
function ParseSteamConfig(FileName: string; var Paths: TArrayOfString): Boolean;
var
Lines: TArrayOfString;
I: Integer;
Line: string;
P: Integer;
Key: string;
Value: string;
Count: Integer;
begin
Result := LoadStringsFromFile(FileName, Lines);
Count := 0;
for I := 0 to GetArrayLength(Lines) - 1 do
begin
Line := Trim(Lines[I]);
if Copy(Line, 1, 1) = '"' then
begin
Delete(Line, 1, 1);
P := Pos('"', Line);
if P > 0 then
begin
Key := Trim(Copy(Line, 1, P - 1));
Delete(Line, 1, P);
Line := Trim(Line);
Log(Format('Found key "%s"', [Key]));
if (CompareText(
Copy(Key, 1, Length(BaseInstallFolderKeyPrefix)),
BaseInstallFolderKeyPrefix) = 0) and
(Line[1] = '"') then
begin
Log(Format('Found base install folder key "%s"', [Key]));
Delete(Line, 1, 1);
P := Pos('"', Line);
if P > 0 then
begin
Value := Trim(Copy(Line, 1, P - 1));
StringChange(Value, '\\', '\');
Log(Format('Found base install folder "%s"', [Value]));
Inc(Count);
SetArrayLength(Paths, Count);
Paths[Count - 1] := Value;
end;
end;
end;
end;
end;
end;

Related

inno setup - Create ini key even if it exists

I need to create multiple keys with the same name, without overwriting the existing keys with that name, preferably right below or above them. For example, a key named App already exists in the ini, I need to create another key (or several) named App without overwriting the existing one. As far as I understand that's impossible to do from the [ini] section. So, how can I force create a new key even if it exists?
There is no built-in function for this because what you're asking for breaks the INI file format. In INI files, within each section, every key name must be unique, which you are going to violate. But the following function might do what you want:
[Code]
procedure AppendKey(const FileName, Section, KeyName, KeyValue: string);
var
S: string;
I: Integer;
CurLine: string;
LineIdx: Integer;
SnFound: Boolean;
Strings: TStringList;
begin
Strings := TStringList.Create;
try
S := Format('[%s]', [Section]);
Strings.LoadFromFile(FileName);
SnFound := False;
LineIdx := Strings.Count;
for I := Strings.Count - 1 downto 0 do
begin
CurLine := Trim(Strings[I]);
// if the iterated line is a section, then...
if (Length(CurLine) > 2) and (CurLine[1] = '[') and (CurLine[Length(CurLine)] = ']') then
begin
// if the iterated line is the section we are looking for, then...
if CompareText(S, CurLine) = 0 then
begin
SnFound := True;
Break;
end;
end
else
if CurLine = '' then
LineIdx := I;
end;
if not SnFound then
begin
Strings.Add(S);
Strings.Add(Format('%s=%s', [KeyName, KeyValue]));
end
else
Strings.Insert(LineIdx, Format('%s=%s', [KeyName, KeyValue]));
Strings.SaveToFile(FileName);
finally
Strings.Free;
end;
end;
Call it like:
AppendKey('C:\File.ini', 'Section', 'KeyName', 'KeyValue');

Delphi - parsing memo after certain String

I have a memo lines like this:
Mahogany
Unpolished
In Stock : Yes
Total Stock : 102
Redwood
Polished
In Stock : Yes
Total Stock : 80
Pine
Polished
In Stock : Yes
Total Stock : 22
And i want to have only the line of Redwood's Total Stock.
Since there are many same string of
Total Stock
I can not use this string as my keyword. So, i use "Redwood" , but i don't know how to get the line of "Total Stock" after "Redwood".
var
i: Integer;
s: string;
begin
for i := 0 to mem0.lines.Count - 1 do
begin
if (AnsiContainsStr(mem0.lines[i], 'Redwood')) then
begin
// s:= Redwood's total stock, how to do this ?
end
end;
end;
The missing code is:
s := mem0.Lines[i+3];
This makes the assumption that the format of the data is always exactly as seen in the question. If that assumption is valid then this simple code is the best solution.
It would probably make more sense to use one of the standard human readable structured data formats like JSON or YAML that have good parsers and emitters. Sadly the support for YAML on Delphi is essentially non-existant so that leaves JSON.
You could try this, but like your original code it's a bit 'fragile' in that it makes assumptions about the layout of the text you're searching (in particular that the text you're searching through is packaged in a TStrings object):
function TotalForItem(const ItemName : String; Strings : TStrings) : String;
var
i,
j,
p : Integer;
s : string;
TotalLineIntro : String;
begin
Result := '';
TotalLineIntro := 'Total Stock : ';
for i := 0 to Strings.Count - 1 do
begin
if (Pos(ItemName, Strings[i]) > 0) then
begin
for j:= i + 1 to Strings.Count - 1 do begin
p := Pos(TotalLineIntro, Strings[j]);
if p > 0 then
begin
Result := Copy(Strings[j], p + Length(TotalLineIntro), Length(Strings[j]));
exit;
end;
end;
end
end;
end;
procedure TForm3.Button1Click(Sender: TObject);
begin
ShowMessage(TotalForItem('Redwood', Memo1.Lines));
end;
Instead of AnsiContainsStr, you can use StartsWith, since the rest of the lines of the Redwood part are indentend.
var
I: Integer;
TotalStockLine: string;
MyArray: TArray<string>
begin
for I := 0 to Memo1.Lines.Count - 1 do
if Memo.Lines[I].StartsWith('Redwood') then
begin
TotalStockLine := Trim(Memo.Lines[I + 3];
Break;
end;
if TotalStockLine <> '' then
begin
MyArray := TotalStockLine.Split([':']);
TotalStock := StrToInt(Trim(MyArray[1]));
end;
etc... This can probably be simplified a little, but that shows how you could do this.
FWIW, this assumes you are using XE3 or later. Otherwise you must use the standalone StartsWith.

Add String of TSearchrec to Memo

I want to add the files in the selected folder to the memobox or in a stringlist and show the results. In both ways, i can add them but i can't show the files from the folder in the memo or from the stringlist in a ShowMessage-dialog.
function CountFilesInFolder(AFolder: String; AMask: String): Integer;
var
tmp1: TSearchRec;
ergebnis: Integer;
memo1: string;
list : TStringList;
begin
result := 0;
if (AFolder <> '') then
begin
if AFolder[length(AFolder)] <> '\' then AFolder := AFolder + '\';
ergebnis := FindFirst(AFolder + AMask, faArchive + faReadOnly + faHidden + faSysFile, tmp1);
while ergebnis = 0 do
begin
Inc(result);
ergebnis := FindNext(tmp1);
while ((tmp1.Name = '|*_tif.tif')) and (ergebnis <> 0) do
ergebnis := FindNext(tmp1);
end;
list.Add(tmp1.Name);
FindClose(tmp1);
end;
end;
thank you for your time and sorry for my bad english.
A low-level function like this should not directly add items to a memo. Instead pass a TStrings (an abstraction of a string list) into the function and fill it:
function CountFilesInFolder(AFolder: String; AMask: String; FileNames: TStrings): Integer;
begin
// do your file enumeration
// for each file call FileNames.Add(FileName);
end;
Since the Lines property of a memo is also of type TStrings you can use it directly like this:
CountFilesInFolder('D:\', '*.TXT', Memo1.Lines);
If you wanted to have the filenames in a string list, the usual pattern goes like this:
FileNames := TStringList.Create;
try
CountFilesInFolder('D:\', '*.TXT', FileNames);
finally
FileNames.Free;
end;
The important point is that the caller creates and destroys the TStringList passed into CountFilesInFolder - an important principle in Delphi.

How do I store and load a list of key-value pairs in a string?

I have a list of strings and the values they are to be replaced with. I'm trying to combine them in a list like 'O'='0',' .'='.', ... so it's easy for me to edit it and add more pairs of replacements to make.
Right now the best way I could think of it is:
var
ListaLimpeza : TStringList;
begin
ListaLimpeza := TStringList.Create;
ListaLimpeza.Delimiter := '|';
ListaLimpeza.QuoteChar := '"';
ListaLimpeza.DelimitedText := 'O=0 | " .=."';
ShowMessage('1o Valor = '+ListaLimpeza.Names[1]+' e 2o Valor = '+ListaLimpeza.ValueFromIndex[1]);
This works, but it's not good for visuals, since I can't code the before string (for ex ' .') like that (which is very visual for the SPACE character), only like (" .) so that the = works to assign a name and value in the TStringList.
The Names and Values by default have to be separated by =, in the style of Windows INI files. There's no way AFAICT to change that separator. As #SirRufo indicates in the comment (and which I had never noticed), you can change that using the TStringList.NameValueSeparator property.
This will give you an idea of what Delphi thinks is in your TStringList, which is not what you think it is:
procedure TForm1.FormCreate(Sender: TObject);
var
SL: TStringList;
Temp: string;
i: Integer;
begin
SL := TStringList.Create;
SL.Delimiter := '|';
SL.QuoteChar := '"';
SL.StrictDelimiter := True;
SL.DelimitedText := 'O=0 | ! .!=!.!';
Temp := 'Count: ' + IntToStr(SL.Count) + #13;
for i := 0 to SL.Count - 1 do
Temp := Temp + Format('Name: %s Value: %s'#13,
[SL.Names[i], SL.ValueFromIndex[i]]);
ShowMessage(Temp);
end;
This produces this output:
TStringList Names/Values probably isn't going to do what you need. It's not clear what your actual goal is, but it appears that a simple text file with a simple list of text|replacement and plain parsing of that file would work, and you can easily use TStringList to read/write from that file, but I don't see any way to do the parsing easily except to do it yourself. You could use an array to store the pairs when you parse them:
type
TReplacePair = record
TextValue: string;
ReplaceValue: string;
end;
TReplacePairs = array of TReplacePair;
function GetReplacementPairs: TReplacePairs;
var
ConfigInfo: TStringList;
i, Split: Integer;
begin
ConfigInfo := TStringList.Create;
try
ConfigInfo.LoadFromFile('ReplacementPairs.txt');
SetLength(Result, ConfigInfo.Count);
for i := 0 to ConfigInfo.Count - 1 do
begin
Split := Pos('|`, ConfigInfo[i];
Result[i].TextValue := Copy(ConfigInfo[i], 1, Split - 1);
Result[i].ReplaceValue := Copy(ConfigInfo[i], Split + 1, MaxInt);
end;
finally
ConfigInfo.Free;
end;
end;
You can then populate whatever controls you need to edit/add/delete the replacement pairs, and just reverse the read operation to write them back out to save.

InnoSetup: don't uninstall changed files

How to tell InnoSetup to not uninstall (text) files which had been changed by the user (== are different from those installed by InnoSetup)?
Or maybe more difficult: when installing a new version over an existing, InnoSetup should ask the user whether to overwrite the changed file, but on a pure uninstall, it should uninstall it without asking.
I recently had a similar problem. This was my solution to detect if a text file (profile) has been changed from the one installed during the last installation run:
Use ISPP (Inno Setup Pre-Processor) to create the list of text files and their hashes at compile time:
[Files]
; ...
#define FindHandle
#define FindResult
#define Mask "Profiles\*.ini"
#sub ProcessFoundFile
#define FileName "Profiles\" + FindGetFileName(FindHandle)
#define FileMd5 GetMd5OfFile(FileName)
Source: {#FileName}; DestDir: {app}\Profiles; Components: profiles; \
Check: ProfileCheck('{#FileMd5}'); AfterInstall: ProfileAfterInstall('{#FileMd5}');
#endsub
#for {FindHandle = FindResult = FindFirst(Mask, 0); FindResult; FindResult = FindNext(FindHandle)} ProcessFoundFile
At the top of the "Code" section I define some useful things:
[Code]
var
PreviousDataCache : tStringList;
function InitializeSetup() : boolean;
begin
// Initialize global variable
PreviousDataCache := tStringList.Create();
result := TRUE;
end;
function BoolToStr( Value : boolean ) : string;
begin
if ( not Value ) then
result := 'false'
else
result := 'true';
end;
In the "Check" event handler I compare the hashes of previous install and current file:
function ProfileCheck( FileMd5 : string ) : boolean;
var
TargetFileName, TargetFileMd5, PreviousFileMd5 : string;
r : integer;
begin
result := FALSE;
TargetFileName := ExpandConstant(CurrentFileName());
Log('Running check procedure for file: ' + TargetFileName);
if not FileExists(TargetFileName) then
begin
Log('Check result: Target file does not exist yet.');
result := TRUE;
exit;
end;
try
TargetFileMd5 := GetMd5OfFile(TargetFileName);
except
TargetFileMd5 := '(error)';
end;
if ( CompareText(TargetFileMd5, FileMd5) = 0 ) then
begin
Log('Check result: Target matches file from setup.');
result := TRUE;
exit;
end;
PreviousFileMd5 := GetPreviousData(ExtractFileName(TargetFileName), '');
if ( PreviousFileMd5 = '' ) then
begin
r := MsgBox(TargetFileName + #10#10 +
'The existing file is different from the one Setup is trying to install. ' +
'It is recommended that you keep the existing file.' + #10#10 +
'Do you want to keep the existing file?', mbConfirmation, MB_YESNO);
result := (r = idNo);
Log('Check result: ' + BoolToStr(result));
end
else if ( CompareText(PreviousFileMd5, TargetFileMd5) <> 0 ) then
begin
r := MsgBox(TargetFileName + #10#10 +
'The existing file has been modified since the last run of Setup. ' +
'It is recommended that you keep the existing file.' + #10#10 +
'Do you want to keep the existing file?', mbConfirmation, MB_YESNO);
result := (r = idNo);
Log('Check result: ' + BoolToStr(result));
end
else
begin
Log('Check result: Existing target has no local modifications.');
result := TRUE;
end;
end;
In the "AfterInstall" event handler I mark the file hash to be stored in
Registry later. Because in my tests the event was triggered even if the file move failed (target file is read-only) I compare the hash again to find out if the file move was successful:
procedure ProfileAfterInstall( FileMd5 : string );
var
TargetFileName, TargetFileMd5 : string;
begin
TargetFileName := ExpandConstant(CurrentFileName());
try
TargetFileMd5 := GetMd5OfFile(TargetFileName);
except
TargetFileMd5 := '(error)';
end;
if ( CompareText(TargetFileMd5, FileMd5) = 0 ) then
begin
Log('Storing hash of installed file: ' + TargetFileName);
PreviousDataCache.Add(ExtractFileName(TargetFileName) + '=' + FileMd5);
end;
end;
procedure RegisterPreviousData( PreviousDataKey : integer );
var
Name, Value : string;
i, n : integer;
begin
for i := 0 to PreviousDataCache.Count-1 do
begin
Value := PreviousDataCache.Strings[i];
n := Pos('=', Value);
if ( n > 0 ) then
begin
Name := Copy(Value, 1, n-1);
Value := Copy(Value, n+1, MaxInt);
SetPreviousData(PreviousDataKey, Name, Value);
end;
end;
end;
Inno can't do this check natively.
To not replace changed files during install, you'll need to use custom [Code] to do a checksum and compare against a known good value that is precomputed or saved from the previous install.
To avoid removing them during uninstall, you'll need to disable Inno's own uninstall for that file and check against the same checksum before removing them, again in [Code].
Note that it's better to keep any files the user can edit outside of the setup to handle this situation better and to correctly adhere to the application guidelines.

Resources