5

How to properly work with FileRead, FileWrite, buffers (or with TFileStream).

I need to read a whole text file to a String, then to write back a String to this file (replace it with a new string).

tshepang
  • 12,111
  • 21
  • 91
  • 136
maxfax
  • 4,281
  • 12
  • 74
  • 120
  • 4
    Are you sure you want to write an entire text file to a string? I would use a `TStringList` or -- if you prefer a more basic approach -- an array of strings. – Andreas Rejbrand Jun 18 '11 at 17:49
  • I'd use a FileStream. Especially when you are on D2009 or higher, as the FileRead and related methods do not support Unicode. – Marjan Venema Jun 18 '11 at 17:50
  • @Marjan: That entire depends on how you define 'support'. It is not at all difficult to use old Pascal I/O to read and write UTF-8 files, for instance, in Delphi 2009+. – Andreas Rejbrand Jun 18 '11 at 17:52
  • 1
    @Andreas: Yes, you can set it up so you send Read/Write(Ln) the correct byte values to put in a file, but that's not what I call support. Try to port a simple Open, Write some lines using WriteLn(somestring, somestring), Close where somestring now may contain non-ASCII and you are out of luck. Also, Embarcadero discourages use of the "old" routines and advises to use Streams for all string related operations to/from files. – Marjan Venema Jun 18 '11 at 17:57
  • @Andreas Rejbrand, using TStringList is incorrect because of implicit line breaks transform. Re: UTF-8 - UTF8Decode and UTF8Encode are with us since at least D7 (maybe even more) – Premature Optimization Jun 18 '11 at 17:58
  • @Marjan: That's easy: `Writeln(txt, UTF8Encode(SomeStr));` – Andreas Rejbrand Jun 18 '11 at 17:59
  • @Downvoter: that is not a reason to call it incorrect. Just something you need to be aware of and possibly (!) work around. – Marjan Venema Jun 18 '11 at 18:00
  • @Andreas: would you care to do that for each and every WriteLn in a 1Million LOC app plus its 50 siblings? – Marjan Venema Jun 18 '11 at 18:00
  • @Marjan Venema, no, its a lossy transform, unless you write you own preserving class. – Premature Optimization Jun 18 '11 at 18:04
  • @Downvoter: and only a concern if you are dealing with multi-platform issues. – Marjan Venema Jun 18 '11 at 18:05
  • @Downvoter step into the light: Since we don't know the exact problem the OP is facing, we cannot know for sure if a `TStringList` would work or not, but given the small amount of information given by the OP, it could very well work. It's even likely. – Andreas Rejbrand Jun 18 '11 at 18:06
  • @Marjan Venema, not at all, TStrings cares about platform only on global level, while arbitrary file can be arbitrary, and will not be exact copy after seemingly unchanging operations like loading into TStrings descendant and saving it back to file. @Andreas Rejbrand, dunno, by some reason OP mentioned these low-level function family, agree with your point, though, TStrings descendants might apply as solution. – Premature Optimization Jun 18 '11 at 18:39
  • @Downvoter: I know that. And an arbitrary file is also only a concern if it could contain line-endings as per a different platform. So TStrings/TStringList's conversion is still only an issue if you are dealing with multi-platform concerns: like getting files from a Mac or a Linux system. If you do not have to deal with that kind of multi-platform issues, then the TStrings/TStringlist's silent conversion is a non-issue. – Marjan Venema Jun 18 '11 at 18:57
  • @Marjan Venema, yes, but conclusion is wrong. I can write file with strange platform flavour right now and it will load seamlessly in TStringList but fail hash check regardless if there were changes to the items or not. – Premature Optimization Jun 18 '11 at 19:34
  • @Downvoter: No, my conclusion is fine. "Writing a file with strange platform flavour" in my book is deliberately creating a multi-platform issue... – Marjan Venema Jun 18 '11 at 19:48
  • Guys, so hot discussion :) I asked by reason of I needed to open a file with a share mode, to learn about how to work with buffers in this case and what could suggested me professionals. The best way that I have found is to use TFileStream+TStringStream=the most convenient way for me. – maxfax Jun 19 '11 at 05:57
  • 1
    @maxfax the question didn't mention share mode – David Heffernan Jun 19 '11 at 06:47

5 Answers5

11

TStringList is what you want if you need to deal with the file on a per-line basis.

If you just want to treat it as a single string blob then there is TStringStream.

Stream := TStringStream.Create('', TEncoding.UTF8);
Try
  Stream.LoadFromFile('c:\desktop\in.txt');
  ShowMessage(Stream.DataString);

  Stream.Clear;
  Stream.WriteString('Greetings earthlings!');
  Stream.SaveToFile('c:\desktop\out.txt');
Finally
  Stream.Free;
End;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
5

The simplest, most fool-proof way to read a file into a string is to use a TStringList like so:

sl := TStringList.Create;
try
    sl.LoadFromFile('C:\myfile.txt');

    //String can be read and modified using the Text property:
    oldString := sl.Text;
    sl.Text := 'foo bar';

    //Text can be written back to the file using:
    sl.WriteToFile('C:\myfile.txt');
finally
    sl.Free;
end;

As has been noted in the comments below your question, this could end up transforming line breaks within your string - so depending on what you're trying to read / do, you will need to look out for this.

Steve Mayne
  • 22,285
  • 4
  • 49
  • 49
2

Some of my utility routines (you can download the code in full from my web site)...

//------------------------------------------------------------------------------
// CsiStrToBytes
//
// Convert pInStr to an array of bytes using the string encoding
// pStringEncoding (one of automatic, Ansi, UTF-16, or UTF-8) and optionally
// include the byte order mark according to the pIncludeBom flag
//------------------------------------------------------------------------------
function CsiStrToBytes(const pInStr: string;
                       pStringEncoding: TECsiStringEncoding;
                       pIncludeBom: Boolean): TByteDynArray;
var
{$IFDEF UNICODE}
  lStringEncoding: TECsiStringEncoding;
  lStringStream: TStringStream;
  lPreambleBytes: TBytes;
  lStringBytes: TBytes;
  lPreambleLen: Integer;
  lStringLen: Integer;
{$ENDIF}
  lLen: Integer;
{$IFDEF UNICODE}
  lIndex: Integer;
{$ENDIF}
begin
  if pInStr <> '' then begin
{$IFDEF UNICODE}
    if pStringEncoding = seAuto then
      lStringEncoding := CsiGetPreferredEncoding(pInStr)
    else
      lStringEncoding := pStringEncoding;

    // UTF-8 and UTF-16 encoding can be handled by the TStringStream class
    if (lStringEncoding = seUtf8) or (lStringEncoding = seUtf16) then begin
      if lStringEncoding = seUtf8 then
        lStringStream := TStringStream.Create(pInStr, TEncoding.Utf8)
      else
        lStringStream := TStringStream.Create(pInStr, TEncoding.Unicode);
      try
        // add the UTF-8 or UTF-16 byte order mark to the start of the array of
        // bytes if required
        if pIncludeBom then
          lPreambleBytes := lStringStream.Encoding.GetPreamble
        else
          SetLength(lPreambleBytes, 0);
        lStringBytes := lStringStream.Bytes;
        lPreambleLen := Length(lPreambleBytes);
        lStringLen := Length(lStringBytes);
        SetLength(Result, lPreambleLen + lStringLen);
        if lPreambleLen > 0 then
          Move(lPreambleBytes[0], Result[0], lPreambleLen);
        if lStringLen > 0 then
          Move(lStringBytes[0], Result[lPreambleLen], lStringLen);
      finally
        lStringStream.Free;
      end;

    end else begin
{$ENDIF}
      // Ansi encoding must be handled manually
      lLen := Length(pInStr);
      SetLength(Result, lLen);
{$IFDEF UNICODE}
      for lIndex := 1 to lLen do
        Result[lIndex - 1] := Ord(pInStr[lIndex]) and $00ff;
{$ELSE}
      Move(pInStr[1], Result[0], lLen);
{$ENDIF}
{$IFDEF UNICODE}
    end;
{$ENDIF}

  end else
    SetLength(Result, 0);
end;

//------------------------------------------------------------------------------
// CsiSaveToFile
//
// Saves pData, an array of bytes, to pFileName
//------------------------------------------------------------------------------
procedure CsiSaveToFile(const pData: TByteDynArray; const pFileName: string);
var
  lFileStream: TFileStream;
  lLen: Integer;
begin
  lFileStream := TFileStream.Create(pFileName, fmCreate);
  try
    lLen := Length(pData);
    if lLen > 0 then
      lFileStream.WriteBuffer(pData[0], lLen);
  finally
    lFileStream.Free;
  end;
end;

//------------------------------------------------------------------------------
// CsiSaveToFile
//
// Saves pText to pFileName using the string encoding pStringEncoding, which is
// one of automatic, Ansi, UTF-16, or UTF-8
//------------------------------------------------------------------------------
procedure CsiSaveToFile(const pText: string; const pFileName: string;
                        pStringEncoding: TECsiStringEncoding);
begin
  CsiSaveToFile(CsiStrToBytes(pText, pStringEncoding), pFileName);
end;
Misha
  • 1,816
  • 1
  • 13
  • 16
2

Here are two functions doing what you want:

function StringFromFile(const FileName: TFileName): RawByteString;
var F: THandle;
    Size: integer;
begin
  result := '';
  if FileName='' then
    exit;
  F := FileOpen(FileName,fmOpenRead or fmShareDenyNone);
  if PtrInt(F)>=0 then begin
{$ifdef LINUX}
    Size := FileSeek(F,0,soFromEnd);
    FileSeek(F,0,soFromBeginning);
{$else}
    Size := GetFileSize(F,nil);
{$endif}
    SetLength(result,Size);
    if FileRead(F,pointer(Result)^,Size)<>Size then
      result := '';
    FileClose(F);
  end;
end;

function FileFromString(const Content: RawByteString; const FileName: TFileName;
  FlushOnDisk: boolean=false): boolean;
var F: THandle;
    L: integer;
begin
  result := false;
  F := FileCreate(FileName);
  if PtrInt(F)<0 then
    exit;
  if pointer(Content)<>nil then
    L := FileWrite(F,pointer(Content)^,length(Content)) else
    L := 0;
  result := (L=length(Content));
{$ifdef MSWINDOWS}
  if FlushOnDisk then
    FlushFileBuffers(F);
{$endif}
  FileClose(F);
end;

They use low-level FileOpen/FIleSeek/FileRead/FileWrite functions.

And you can specify any fmShare* option you need.

It uses a RawByteString type, so it expects the text to be handled in a bytes-oriented way. It won't work with Unicode text file, but with Ansi Text. You'll have to set the appropriate code page if you want to interact with it using the string type since Delphi 2009.

Before Delphi 2009, just define:

type
  RawByteString = AnsiString;
Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • I was tempted to vote -1 because you did not send the code for GetFileSize. But I used your linux calculation to get file size, so voting +1. Thank you – John Boe Sep 16 '18 at 16:38
1

Added requested example of working with FileXXX function family too. Note on using RawByteString and file I/O - it is perfectly valid. Another note: for brevity in the low-level code i omitted some error-checking assertions for less likely occurring errors. Caveat emptor!

const
  FileName = 'Unit14.pas';  // this program is a stuntmaster,
                            // reads and writes its own source

procedure TForm14.FormClick(Sender: TObject);
var
  Stream: TFileStream;
  Buffer: RawByteString;
begin
  Stream := TFileStream.Create(FileName, fmOpenReadWrite or fmShareExclusive);
  // read entire file into string buffer
  SetLength(Buffer, Stream.Size);
  Stream.ReadBuffer(Buffer[1], Stream.Size);

  // do something with string
  OutputDebugString(PChar(Format('Buffer = "%s"', [Buffer])));

  // prepare to write
  Stream.Position := 0;   // rewind file pointer
  Stream.Size := 0;       // truncate the file

  // write entire string into the file
  Stream.WriteBuffer(Buffer[1], Length(Buffer));

  Stream.Free;
end;

// on right click - do exactly the same but using low-level FileXXX calls
procedure TForm14.FormContextPopup(Sender: TObject; MousePos: TPoint; var
    Handled: Boolean);
var
  Handle: Integer;
  Size: Cardinal;
  Buffer: RawByteString;
  Transferred: Integer;
begin
  Handle := FileOpen(FileName, fmOpenReadWrite or fmShareExclusive);
  Assert(Handle >= 0);

  // read entire file into string buffer
  Size := GetFileSize(Handle, nil);
  Assert(Size <> INVALID_FILE_SIZE);
  SetLength(Buffer, Size);
  Transferred := FileRead(Handle, Buffer[1], Size);
  Assert(not (Transferred < Size));

  // do something with string
  OutputDebugString(PChar(Format('Buffer = "%s"', [Buffer])));

  // prepare to write
  FileSeek(Handle, 0, 0); // rewind file pointer
  SetEndOfFile(Handle);   // truncate the file

  // write entire string into the file
  Transferred := FileWrite(Handle, Buffer[1], Length(Buffer));
  Assert(not (Transferred < Length(Buffer)));

  FileClose(Handle);
end;
Premature Optimization
  • 1,917
  • 1
  • 15
  • 24