9

I have a file .\input.txt like this:

aaa
bbb
ccc

If I read it using TStrings.LoadFromFile and write it back (even without applying any changes) using TStrings.SaveToFile, it creates an empty line at the end of the output file.

var
  Lines : TStrings;
begin
  Lines := TStringList.Create;
  try
    Lines.LoadFromFile('.\input.txt');

    //...

    Lines.SaveToFile('.\output.txt');
  finally
    Lines.Free;
  end;
end;

The same behavior can be observed using the TStrings.Text property which will return a string containing an empty line at its end.

Boann
  • 44,932
  • 13
  • 106
  • 138
Fabrizio
  • 6,598
  • 5
  • 31
  • 75
  • just wondering, why on earth would you want to write it back even when there is no change applied in the file? why not just simply read it? – Bilal Ahmed Oct 17 '19 at 07:27
  • 3
    @BilalAhmed: sure, it is a simplified test, the same empty line appear when applying changes to the string list – Fabrizio Oct 17 '19 at 07:29
  • By "creates an empty line" I guess you mean that your original file does not end with the `\n` character and the function adds the `\n` to the file? Or does the function literally add a `\n` right after an existing `\n` at the end of file? POSIX requires text files to have all their lines terminated by a `\n`, just fyi. Lots of software was written to follow some standards so that's why a lot of editors will add the missing terminating `\n` when you save files by default (e.g. `vim`, IDEs etc all by default make your files POSIX-compliant.) – Giacomo Alzetta Oct 17 '19 at 15:49

2 Answers2

13

For Delphi 10.1 and newer there is a property TrailingLineBreak controlling this behavior.

When TrailingLineBreak property is True (default value) then Text property will contain line break after last line. When it is False, then Text value will not contain line break after last line. This also may be controlled by soTrailingLineBreak option.

Uwe Raabe
  • 39,888
  • 3
  • 77
  • 115
  • Great information, I'm working on Delphi2007 and DelphiXE7 but I'll surely be glad to use the `TrailingLineBreak` property as soon as I upgrade the IDE. +1 and accepted – Fabrizio Oct 17 '19 at 08:01
1

For Delphi 10.1 (Berlin) or newer, the best solution is described in Uwe's answer.

For older Delphi versions, I found a solution by creating a child class of TStringList and overriding the TStrings.GetTextStr virtual function but I will be glad to know if there is a better solution or if someone else found something wrong in my solution

Interface:

  uses
    Classes;

  type
    TMyStringList = class(TStringList)
    private
      FIncludeLastLineBreakInText : Boolean;
    protected
      function GetTextStr: string; override;
    public
      constructor Create(AIncludeLastLineBreakInText : Boolean = False); overload;
      property IncludeLastLineBreakInText : Boolean read FIncludeLastLineBreakInText write FIncludeLastLineBreakInText;
    end;

Implementation:

uses
  StrUtils;      

constructor TMyStringList.Create(AIncludeLastLineBreakInText : Boolean = False);
begin
  inherited Create;

  FIncludeLastLineBreakInText := AIncludeLastLineBreakInText;
end;

function TMyStringList.GetTextStr: string;
begin
  Result := inherited;

  if(not IncludeLastLineBreakInText) and EndsStr(LineBreak, Result)
  then SetLength(Result, Length(Result) - Length(LineBreak));
end;

Example:

procedure TForm1.Button1Click(Sender: TObject);
var
  Lines : TStrings;
begin
  Lines := TMyStringList.Create();
  try
    Lines.LoadFromFile('.\input.txt');
    Lines.SaveToFile('.\output.txt');
  finally
    Lines.Free;
  end;
end;
Fabrizio
  • 6,598
  • 5
  • 31
  • 75
  • 7
    It is worth pointing out that your code occasionally does `SetLength(Result, -2)`. – Andreas Rejbrand Oct 17 '19 at 07:30
  • @AndreasRejbrand: What do you mean? – Fabrizio Oct 17 '19 at 09:44
  • 1
    In your `GetTextStr`, if `Length(Result)` is `0`, then you do `SetLength(Result, -2)`, which is bad. It might be the case that the effect is the same as `SetLength(Result, 0)`, but I know of no guarantee regarding that. The official documentation, at least, doesn't contain any such guarantee. (So in theory bad things could happen.) – Andreas Rejbrand Oct 17 '19 at 09:53
  • @AndreasRejbrand: You're perfectly right, I did not understand. I've fixed the code in the answer, thanks for the explanation – Fabrizio Oct 17 '19 at 10:04
  • 2
    But now you still got another bug! If `Length(Result) = 1`, then you do `SetLength(Result, -1)`, which is equally bad! In addition, it might be the case that `Result` doesn't end with a line break, in which case you will remove the two last characters from the last line. That's also a bug. (And that might happen, for instance, if you use `TrailingLineBreak`, I suspect. Even if not, there might be other instances.) You really should test if the string really ends with a line break, like `if not IncludeLastLineBreakInText and Result.EndsWith(LineBreak) then`. – Andreas Rejbrand Oct 17 '19 at 10:07
  • 1
    @AndreasRejbrand: I took it for granted that in presence of any char, the TStrings would add at least a LineBreak, but this behavior could change in future. Answer updated again, thanks – Fabrizio Oct 17 '19 at 10:20
  • 1
    I'm sorry, but the new condition is still wrong... :( `Pos(LineBreak, Result) = Length(Result) - Length(LineBreak) + 1`. `Pos` gives the index of the first match. If you string contains 6 line breaks, it will give the position of the first one, but you clearly expect the last one... – Andreas Rejbrand Oct 17 '19 at 10:31
  • 1
    `Result.EndsWith(LineBreak)` works, as does `EndsStr(LineBreak, Result)`, as does `RightStr(Result, 2) = LineBreak`, as does `Copy(Result, Length(Result) - 1, 2) = LineBreak`. – Andreas Rejbrand Oct 17 '19 at 10:35
  • @AndreasRejbrand: My error again, updated using EndsStr, thanks – Fabrizio Oct 17 '19 at 10:43
  • 1
    I would ditch the constructor overload and make the property default to true. After all whole purpose of existence of this descendant is that. – Sertac Akyuz Oct 17 '19 at 14:51