How do I prevent TStrings.SaveToFile creating a final empty line?
Asked Answered
S

3

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.

Scrouge answered 17/10, 2019 at 7:23 Comment(3)
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?Bodleian
@BilalAhmed: sure, it is a simplified test, the same empty line appear when applying changes to the string listScrouge
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.)Losel
L
17

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.

Lancey answered 17/10, 2019 at 7:51 Comment(1)
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 acceptedScrouge
S
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;
Scrouge answered 17/10, 2019 at 7:23 Comment(10)
It is worth pointing out that your code occasionally does SetLength(Result, -2).Butacaine
@AndreasRejbrand: What do you mean?Scrouge
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.)Butacaine
@AndreasRejbrand: You're perfectly right, I did not understand. I've fixed the code in the answer, thanks for the explanationScrouge
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.Butacaine
@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, thanksScrouge
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...Butacaine
Result.EndsWith(LineBreak) works, as does EndsStr(LineBreak, Result), as does RightStr(Result, 2) = LineBreak, as does Copy(Result, Length(Result) - 1, 2) = LineBreak.Butacaine
@AndreasRejbrand: My error again, updated using EndsStr, thanksScrouge
I would ditch the constructor overload and make the property default to true. After all whole purpose of existence of this descendant is that.Lothario
M
0

How about:

Lines.Text:=Trim(Lines.Text);
Manoeuvre answered 25/5, 2023 at 10:51 Comment(1)
Is this a question or an answer?Mohave

© 2022 - 2024 — McMap. All rights reserved.