如何防止 TStrings.SaveToFile 生成最后一个空行?

9
我有一个文件.\input.txt,内容如下:
aaa
bbb
ccc

如果我使用TStrings.LoadFromFile读取它,然后使用TStrings.SaveToFile将其写回(即使没有进行任何更改),它会在输出文件的末尾创建一个空行。
var
  Lines : TStrings;
begin
  Lines := TStringList.Create;
  try
    Lines.LoadFromFile('.\input.txt');

    //...

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

可以使用TStrings.Text属性来观察同样的行为,它会返回一个包含空行的字符串。

我只是想知道,即使文件中没有应用更改,为什么你还想将其写回?为什么不只是简单地读取它呢? - Bilal Ahmed
3
没问题,这只是一份简化版的测试,当对字符串列表进行更改时,会出现同样的空行。 - Fabrizio
“creates an empty line” 的意思是您的原始文件没有以 \n 字符结尾,而该函数会将 \n 添加到该文件中?还是该函数在文件末尾的现有 \n 后面直接添加一个 \n? POSIX 要求文本文件的所有行都以 \n 终止,只是提供信息。许多软件都是按照某些标准编写的,因此许多编辑器在保存文件时默认添加丢失的终止符 \n(例如 vim、IDE 等都默认使您的文件符合 POSIX 规范)。 - Giacomo Alzetta
3个回答

16

对于 Delphi 10.1 及更新版本,有一个名为 TrailingLineBreak 的属性来控制此行为。

当 TrailingLineBreak 属性为 True(默认值)时,Text 属性将在最后一行之后包含换行符。当为 False 时,则 Text 值在最后一行后不包含换行符。这也可以通过 soTrailingLineBreak 选项进行控制。


非常好的信息,我正在使用Delphi2007和DelphiXE7,但是一旦升级了IDE,我肯定会很高兴使用TrailingLineBreak属性。+1并接受。 - Fabrizio

1

对于 Delphi 10.1(Berlin)或更高版本,最好的解决方案在 Uwe 的答案中描述。

对于旧版本的 Delphi,我发现通过创建 TStringList 的子类并覆盖 TStrings.GetTextStr 虚函数的解决方案,但如果有更好的解决方案或其他人发现我的解决方案有问题,我会很高兴知道。

接口:

  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;

例子:

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;

7
值得指出的是,你的代码偶尔会执行SetLength(Result, -2) - Andreas Rejbrand
2
但现在你还有另一个错误!如果 Length(Result) = 1,那么你会执行 SetLength(Result, -1),这同样糟糕!此外,可能情况是 Result 不以换行符结尾,在这种情况下,您将从最后一行中删除最后两个字符。这也是一个错误。(例如,如果使用 TrailingLineBreak,我怀疑会发生这种情况。即使没有,可能会有其他实例。)您真的应该测试字符串是否以换行符结尾,例如 if not IncludeLastLineBreakInText and Result.EndsWith(LineBreak) then - Andreas Rejbrand
1
很抱歉,但新的条件仍然是错误的... :( Pos(LineBreak, Result) = Length(Result) - Length(LineBreak) + 1Pos 给出第一个匹配项的索引。如果您的字符串包含6个换行符,则它将给出第一个换行符的位置,但您显然期望最后一个... - Andreas Rejbrand
1
Result.EndsWith(LineBreak) 可以工作,EndsStr(LineBreak, Result) 也可以,RightStr(Result, 2) = LineBreak 也可以,Copy(Result, Length(Result) - 1, 2) = LineBreak 也可以。 - Andreas Rejbrand
1
我会放弃构造函数重载,并使属性默认为true。毕竟,这个后代存在的整个目的就是如此。 - Sertac Akyuz
显示剩余5条评论

0

这样怎么样:

Lines.Text:=Trim(Lines.Text);

这是一个问题还是一个答案? - Freddie Bell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接