Compare paths in Inno Setup - path

Is there a built-in way to compare two strings representing paths in Inno Setup Pascal? If not with one function, then perhaps via some normalisation of the path.
Naively comparing strings is obviously not correct, even if we ignore case with SameText() (as per the Windows rules).
As a minimum, correct comparison must
Treat \ and / as identical
Ignore multiple separators like \\ (theat them as one, like the OS)
Ignore trailing separators (to compare directory paths correctly, for which it is mainly needed)
Resolve paths (foo\..\bar equals bar, at least if foo exists)
etc. (rules are well known)
Not require the paths actually exist in the file system.
Resolving absolute vs. relative paths is a bonus, but it requires specifying the current path. Perhaps CWD is OK, but I'm not sure Inno accepts relative installation paths anyway.
This must be a fairly common task for an installer, but I'm surprised not to find an easy yet correct solution...

Combine ExpandFileName, AddBackslash and SameText:
function SamePath(P1, P2: string): Boolean;
begin
P1 := AddBackslash(ExpandFileName(P1));
P2 := AddBackslash(ExpandFileName(P2));
Result := SameText(P1, P2);
end;
The ExpandFileName:
Converts / to \.
Normalizes a sequence of any slashes to one backslash (except for leading backslashes in UNC paths).
Resolves relative paths.
The AddBackslash takes care of ignoring the trailing separators.
Tests:
procedure TestSamePath(P: string);
begin
if not SamePath(P, 'C:\my\path\MyProg.exe') then
RaiseException('Test failed: ' + P);
end;
function InitializeSetup(): Boolean;
begin
TestSamePath('C:\my\path\MyProg.exe');
TestSamePath('C:\my\path\MYPROG.exe');
TestSamePath('C:\my\path\\MyProg.exe');
TestSamePath('C:/my/path/MyProg.exe');
TestSamePath('C:\my/path//MyProg.exe');
TestSamePath('C:\my\path\MyProg.exe\');
TestSamePath('C:\my\..\my\path\MyProg.exe');
SetCurrentDir('C:\');
TestSamePath('\my\path\MyProg.exe');
TestSamePath('my\path\MyProg.exe');
SetCurrentDir('C:\my');
TestSamePath('path\MyProg.exe');
TestSamePath('.\path\MyProg.exe');
Result := True;
end;

Related

Which way is the "correct" one to define a shortcut in Delphi?

There are many examples on how to define a ShortCut in a Delphi program, but
they boil down to just two different ways:
Add any of the scCtrl, scShift and scAlt constants to Ord() of the key
Use the Menus.ShortCut function
e.g.
Action.ShortCut := scCtrl + scShift + Ord('K');
// vs
Action.ShortCut := Menus.ShortCut(Word('K'), [ssCtrl, ssShift]);
Is one of these two ways preferable? If yes, which one and why?
The code is almost identical, but ShortCut has some additional checks:
function ShortCut(Key: Word; Shift: TShiftState): TShortCut;
begin
Result := 0;
if HiByte(Key) <> 0 then Exit; // if Key is national character then it can't be used as shortcut
Result := Key;
if ssShift in Shift then Inc(Result, scShift); // this is identical to "+" scShift
if ssCtrl in Shift then Inc(Result, scCtrl);
if ssAlt in Shift then Inc(Result, scAlt);
end;
Because RegisterHotKey function uses Virtual key codes (which has values from $00 to $FE) this additional check is significant.
Note that instead of Ord documentation, real Ord function returns smallint (signed Word), so using national characters can change modificators, that contained in Hi-byte of ShortCut value.
So, more preferably is use ShortCut function.
I'd say that whenever there is a function that does the job, it is better to use the function.
Because given the chance in the future that something changes, having a function gives you a "hard link" to the call, so if the function becomes deprecated, you are notified, and if the function logic changes, you get the update silently.
Otherwise, you will not benefit from this.
Now in this particular case, what are the chances for the definition of a shortcut to change in the next 10-20 years?
Probably none. But I'd still advocate the function call (if not for anything, but you don't have to remember the logic (was it addition or was it logical ORing? one might ask himself later on)

Searching i TStringList using POS - need more advanced method

I use the following function to search a TStringList I am reading from a file.
I know that when I search a value, then the return value I need is always on the line after the one with the item I search.
It has always worked using POS to search, but now the file has been expanded and I have to look for 2 items 'Adresse' and 'Adresse 2'
That gives me an issue since pos finds 'Adresse' in both cases and my data is then wrong.
Is there another method of searching a string for a substring that I don't know of or do I have to make my own.
function FindValue(const aFilename, aSearch: string): string;
var
InfoList: TStringList;
Counter: integer;
begin
InfoList := TStringList.Create;
try
InfoList.LoadFromFile(aFilename);
if InfoList.Count > 0 then
for Counter := 0 to InfoList.Count - 1 do
begin
if Pos(aSearch, Infolist.Strings[Counter]) > 0 then
Result := Infolist.Strings[Counter + 1]
end
else
Result := '';
finally
InfoList.Free;
end;
end;
For info: the input to the TStringList comes from a textfile extracted from a HTML file.
A sample of a file could be:
OZ8HP
Hugo Pedersen
Radioamatør
Nykøbing M
Sendeposition:
Adresse:
Prinsensvej 18
Postnummer:
7900
Bynavn:
Nykøbing M
Antenne højde (m):
Kote (m):?Kote (m):Brugerens/tilladelsesindehaverens øvrige adresseoplysninger så som Stednavn og/eller Postboks. Hjælpetegnet * kan anvendes, som beskrevet i hjælp.
Koordinater:
Geografisk anvendelse:
Frekvensmaske:
Tekniske specifikationer:
Sendeeffekt basisstation:
Sendeeffekt mobile anlæg:
Båndbredde (MHz):
Antal anlæg:
MMSI:
Kaldesignaltype:
Personlig
Frekvenskategori:
Udstedelses-metode:
Intention om overdragelse:
Nej
Udløbsdato:
Brugerdata:
Brugernummer:
956078
Adresse:
Prinsensvej 18
Adresse 2:
Sejerslev
Postnr.:
7900
Bynavn:
Nykøbing M
Kaldesignal-kategori:
Bestået A
It looks to me as though the real mistake is being too lax in your search. Why accept partial matches? It would seem more robust to look for complete matches
if SameText(aSearch, Infolist[Counter]) then
or perhaps to account for leading and trailing whitespace:
if SameText(aSearch, Trim(Infolist[Counter])) then
You'd need to pass 'Adresse:' or 'Adresse 2:' as the search string, or add the colon in the search function.
Use AnsiSameText if you want locale sensitive comparison. Use = if you want case sensitive comparison, etc.
You might pass multiple search strings and be able to loop only once over the file. As it stands you read it twice which seems wasteful. Indeed surely better to operate on a string list and not be coupled to file storage.
You return the last match in the data rather than the first, for instance. What if there are multiple matches? Does your code behave as intended?
You should also be aware that if no match is found your function does not assign to the Result variable which means it is undefined.

How to copy all the words in a paragraph and add each word # on delphi

Using Delphi 7, if I have some text like that shown in the lefthand window below, how could I extract all the words and punctuation in a paragraph and copy them to another window like that on the right, followed by a #?
If I'm understanding you correctly, you need what's called a "tokeniser" or "lexer".
D7 comes with one built-in, in the Classes unit, misleadingly called TParser (misleadingly because parsing normally means the "grammatical analysis" step which may follow the tokenisation of the text, as happens f.i. in the operation of a compiler).
Anyway, iirc, Delphi's TParser was intended to do things like process the text of DFM files, so will not necessarily split the text up exactly as you want, but it's a start. For example, when it tokenises ":=", it will return the ":" and "=" as two separate tokens, but, of course you are free to join them up again when NextToken/TokenString return these in succession. Btw, there are several alternative ways of implementing a tokeniser using classes in the freeware Jedi JCL and JVCL libraries, f.i.
If the text window on the left of your q is in your own app, code like the following
may do what you want:
procedure TForm1.Tokenise;
var
SS : TStringStream;
TokenText : String;
Parser : TParser;
begin
SS := TStringStream.Create(Memo1.Lines.Text);
Parser := TParser.Create(SS);
try
while Parser.Token <> #0 do begin
TokenText := Parser.TokenString;
Memo2.Lines.Add(TokenText + '#');
Parser.NextToken;
end;
finally
Parser.Free;
SS.Free;
end;
end;
If the text window is in another app, you would need a method of retrieving the text from it, too, of course.

Is it OK to use DecimalSeparator to force Floattostr/Strtofloat functions to use a decimal point

Currently, I'm setting DecimalSeparator to a '.' in each procedure which uses these functions.
It would be much easier to set this globally at the start of the program but I found Delphi seems to periodically set this back to the current locale.
I need to make sure that a decimal point is used for all conversions no matter which country the program is used in as this is the standard for this type of program and all files structure and communication protocols, numeric displays in forms/edits etc are required to be formatted in this way.
I've been told in another thread that using decimalseparator is not the correct way to do it but I was not given any alternatives. The other threads concerning this subject that I've read don't seem to offer any formative guidance or are overly complex.
Is there a simple 'correct' way to do this ?
Yes, the DecimalSeparator global variable might be changed by the RTL during runtime, which caused a lot of headache for me a few years ago before I realised this.
The thing is that DecimalSeparator is updated by the RTL when the Windows decimal separator is changed, for instance, using the Control Panel. This might seem like a rather small problem. Indeed, how often does the end user change the system's decimal separator?
The big issue is that the DecimalSeparator variable is updated (according to the system setting) every time you switch user (in Windows). That came as a surprise to me. That is, if your system setting uses a comma (',') as the decimal separator, and you set DecimalSeparator := '.' at application startup, then DecimalSeparator will revert to a comma if you switch user (and you'll notice that when you switch back).
You can tell the RTL not to update the decimal separator by
Application.UpdateFormatSettings := false;
At any rate, there are better alternatives to DecimalSeparator, as discussed in other answers and comments.
I am/was under the assumption that the global DecimalSeperator variable would not be touched by the RTL. If not, then all these routines have an optional parameter FormatSettings which you could use. Globaly declare a TFormatSettings variable and use it for each occurance of these routines.
A small benefit of it could be that the routines are thread-safe when you specify your own format settings.
To be on the safe side, i would use TFormatSettings, this has two advantages:
The formatting is thread safe, other code/libraries cannot influence your function.
You do not influence other code, which possibly rely upon certain settings.
Here a possible implementation:
function FloatToStrWithDecimalPoint(const Value: Extended; const Format: String = '0.###'): String;
var
myFormatSettings: TFormatSettings;
begin
GetLocaleFormatSettings(GetThreadLocale, myFormatSettings);
myFormatSettings.DecimalSeparator := '.';
Result := FormatFloat(Format, Value, myFormatSettings);
end;
You could patch every string before and after calling a RTL function with some ForceLocalSeparator() and ForceDotSeparator() functions.
// before a RTL call
Function ForceLocalSeparator(Const StrValue: String): String;
Var
SepPos: Integer;
Begin
Result := StrValue;
If DecimalSeparator <> '.' Then
Begin
SepPos := Pos( '.', Result );
If SepPos > 0 Then Result[SepPos] := DecimalSeparator;
End;
End;
// after a RTL call
Function ForceDotSeparator(Const StrValue: String): String;
Var
SepPos: Integer;
Begin
Result := StrValue;
If DecimalSeparator <> '.' Then
Begin
SepPos := Pos( DecimalSeparator, Result );
If SepPos > 0 Then Result[SepPos] := '.';
End;
End;
It's OK if you have no alternative. Prefer the versions of those functions that accept a TFormatSettings parameter, if your Delphi version is recent enough, so that you don't interfere with any other code that relies on that global variable for locale-aware behavior.
FloatToStr and StrToFloat are locale-sensitive functions. If you need to convert your floating-point value to a string to persist it somewhere that a program will read later (such as to a file, the registry, or a network socket), then you should use the locale-independent functions Str and Val for your conversions instead. They always use . for the decimal separator, regardless of the DecimalSeparator variable or other environmental settings.

Delphi: how to automatically remove unused vars ("Variable 'x' is declared but never used" hint)

Is there any tool (preferably freeware) that can analyze Pascal/Delphi syntax and automatically remove unused vars?
In my case, I'm working with a very large Delphi code base, and the compiler hints report over one thousand cases of "Variable 'x' is declared but never used".
I would take hours to remove them by hand, and I might make errors, but the right tool should be able to do that safely and automatically.
I searched online but didn't find one... does anybody here know of such a tool?
Thanks...
Mark Brarford
I see your point and totally agree that such a tool would be useful when working with legacy code. Unfortunately I don't know of any existing tool (I should add freeware tool here, static analyis tools should of course be able to do it easily, but I don't know of any free static code analysis tool) that is capable doing that.
But I guess you could easily write such a tool in a few minutes. A small GUI with a memo and a button should be enough. Then just copy the compiler hints to the memo and press the button. The tool then parses every line. It can easily check if the line contains the hint you are looking for and each such line has the same structure, so parsing should be relatively easy. It can then extract the file name and the line number, open the file and remove the variable declaration. This can be a bit tricky in case of multiple variable declarations in one line but I think it is doable.
I don't know if that's too much effort for you compared to the task of removing all variable declarations yourself. But I would like to see such a tool, so feel free to write it :)
Hope that helped at least a bit.
Okay, I really can't see any problems here. For the parsing part:
function ParseHint (const HintText : String; out HintInfo : THintInfo) : Boolean;
var
I, J : Integer;
HintName : String;
begin
Result := False;
for I := 1 to Length (HintText) do
begin
if (HintText [I] = '(') then
begin
J := I + 1;
while (HintText [J] <> ')') do Inc (J);
HintInfo.LineNumber := StrToInt (MidStr (HintText, I+1, J-(I+1)));
HintInfo.SourceFile := MidStr (HintText, 12, I-12);
HintName := MidStr (HintText, J+3, 5);
if (HintName <> 'H2164') then Exit (False);
end;
if (HintText [I] = '''') then
begin
J := I + 1;
while (HintText [J] <> '''') do Inc (J);
HintInfo.VarName := MidStr (HintText, I+1, J-(I+1));
Exit (True);
end;
end;
end;
Well, reading the source file should be easy, so the only remaing part is removing the variable from its line of declaration. We can simply search for occurences of HintInfo.VarName in the line and check if the character before and after the occurence are no letters but only ' ', ',' or ':'. If this is the case we can just remove it. This covers all these cases:
var UnusedVar : Integer;
var
UnusedVar,
AnotherVar : Integer;
var
UnusedVar, AnotherVar : Integer;
Tell me if I'm wrong or if I forgot any cases but I think this would work and woulde solve the problem of removing unused variables from delphi source files using the compiler-generated hints.
The solution is simple, but requires those hours to be sure you don't make a mistake. First off, You can use Alt-F8 to step through each report one after the other (and Alt-F7 to step backwards). That makes locating them very easy. The cursor is put on the line for you. Then just press the '/' key twice to comment it out. Don't delete it, comment it. This way if you make a mistake you haven't lost any information. The presence of the variable and its data type is still recorded. You can tidy it up later at some point.
One caveat to all this: Conditional compilation may render some variables unused when built different ways. If this happens, then just uncomment the variable again, and put the condition around the declaration too.
Are you sure the variables shouldn't be used? I know the compiler figures out that they aren't used right now, but is that correct, perhaps many of these should be used, but a developer used x2 instead of x1 for instance, copy and paste?
While you might want to remove all those variables unscrutinized, I wouldn't be so hasty, they might be indications of bugs in your code that you'd like to fix.
Example:
procedure PlotPixelAtCenter(rect: Rectangle)
var
x, y: Integer;
begin
x := (rect.Left + rect.Right) div 2;
x := (rect.Top + rect.Bottom) div 2; // <-- bug here, should be y :=
PlotPixel(x, y);
end;
In this example you'll get an error about an unused variable, but this is a bug lurking. Of course, in this example the bug should be easy to find since the plotting will probably be off, but other similar bugs might be harder to spot.
If there is no such tool and you have some patience, I'm building a Delphi analysis and repair tool. And removal of unused symbols is on the list. It is a low proirity project so I can't give an estimate on when its ready.
Just to explain why this isn't a trivial task:
read the source
create a model that contains enough information for each symbol usage.
mark all unused symbols.
rewrite the source without the unneeded symbols.
Task 1 and 2 are hard (luckily for me those are already done). The Delphi language is quite complex. And you need all language elements to be able to recreate the source.
Task 3 is simple. Just flag all symbols that are unused. But beware of symbols in the interface section of a unit. They are possibly not used but needed later (or by some other project).
Task 4 depends.
Aproach A uses an intermediate format (for example a stringlist), you can then use the model to find the declaration of each unused symbol (bottom up else you possibly change the line numbers). You delete all not needed. And don't forget to delete the var keyword if it's the last var in the list!
Aproach B completely rewrites the source file. In this case, you must preserve all comments which is not really fun to do (but my model needs that too). You just removes the unused symbols from the model and rewrite it.
Always be sure to create a backup, because this can end up in disaster.

Resources