Ada.Text_IO.Text_Streams and Ada.Text_IO.End_Of_File interact oddly

So, the following program kinda sorta looks like a naive implementation of the simplest concept of the Unix cat program:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Streams, Ada.Text_IO.Text_Streams;

procedure Not_Really_Cat is
   Input_Stream : access Ada.Streams.Root_Stream_Type'Class :=
     Ada.Text_IO.Text_Streams.Stream (File => Ada.Text_IO.Current_Input);
   Output_Stream : access Ada.Streams.Root_Stream_Type'Class :=
     Ada.Text_IO.Text_Streams.Stream (File => Ada.Text_IO.Current_Output);

   C : Character;
begin
   while not End_Of_File loop
      Character'Read (Input_Stream, C);
      Character'Write (Output_Stream, C);
   end loop;
end Not_Really_Cat;

But it is not!

If you do

printf 'Hello!\n' | ./cat | od -c

you get

0000000   H   e   l   l   o   !
0000006

Notice that there is no newline at the end, even though there was a newline in the input.

Looking at this a little closer, it turns out that this happens because the End_Of_File function in use there is from Ada.Text_IO, and it behaves differently from the End_Of_File function that is used on Stream files, as declared in Ada.Streams.Stream_IO.

Here’s another program, that uses Ada.Streams.Stream_IO:

with Ada.Streams, Ada.Text_IO.Text_Streams;
with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO;
with Ada.Command_Line; use Ada.Command_Line;

procedure Try_Streams is
   Input_File : File_Type;
   Input_Stream : access Ada.Streams.Root_Stream_Type'Class;
   Output_Stream : access Ada.Streams.Root_Stream_Type'Class :=
     Ada.Text_IO.Text_Streams.Stream (File => Ada.Text_IO.Current_Output);

   C : Character;

begin
   Open (Input_File, In_File, Argument (1)); --  "../data/try_streams.dat"
   Input_Stream := Stream (Input_File);
   while not End_Of_File (Input_File) loop
      Character'Read (Input_Stream, C);
      Character'Write (Output_Stream, C);
   end loop;
end Try_Streams;

If I pass the name of a file that contains just Hello!\n to this program, it works properly and outputs the final newline.

But as the File_Type from Ada.Text_IO and the File_Type from Ada.Streams.Stream_IO are not compatible, I can’t see any way to use the End_Of_File from Ada.Streams.Streams_IO with a Stream created from Ada.Text_IO.Current_Input by way of the function Ada.Text_IO.Text_Streams.Stream.

Is my understanding correct?

Is there any way to portably obtain a value of Ada.Streams.Stream_IO.File_Type that is associated with the standard input, so that it may be used with Ada.Streams.Stream_IO.End_Of_File?

Forget that End_Of_File even exists. It is inefficient and guaranteed not to work in many cases.

Either use exception (End_Error) or else stream I/O protocol:

with Ada.Streams;               use Ada.Streams;
with Ada.Text_IO.Text_Streams;  use Ada.Text_IO.Text_Streams;
with Ada.Streams.Stream_IO;     use Ada.Streams.Stream_IO;
with Ada.Command_Line;          use Ada.Command_Line;

procedure Main is
   Input_File : File_Type;
begin
   Open (Input_File, In_File, Argument (1));
   declare
      Input  : Root_Stream_Type'Class renames Stream (Input_File).all;
      Output : Root_Stream_Type'Class renames Stream (Ada.Text_IO.Standard_Output).all;
      Buffer : Stream_Element_Array (1..120);
      Last   : Stream_Element_Offset;
   begin
      loop
         Input.Read (Buffer, Last);
         exit when Last < Buffer'First;
         Output.Write (Buffer (1..Last));
      end loop;
   end;
   Close (Input_File);
   Set_Exit_Status (Success);
exception
   when others =>
      Set_Exit_Status (Failure);
end Main;
1 Like

I was not aware that End_Of_File was inefficient; that’s interesting. I agree using a buffer is a much better way to do it in general.

Thanks!

Streams are for heterogenous binary I/O. I don’t know why you’d use them for homogenous text I/O. To copy text from standard input to standard output:

with Ada.Text_IO;

procedure I2O is
begin
   All_Lines : loop
      Ada.Text_IO.Put_Line (Item => Ada.Text_IO.Get_Line);
   end loop All_Lines;
exception -- I2O
when Ada.Text_IO.End_Error =>
   null;
end I2O;

Ada.Text_IO.End_Of_File has the problem that if your file ends with a null line, it returns True before you have read that last line. Handling End_Error avoids that:

$ printf 'Hello!\n' | ./i2o | hd
00000000  48 65 6c 6c 6f 21 0a                              |Hello!.|
00000007
$ printf 'Hello!\n\n' | ./i2o | hd
00000000  48 65 6c 6c 6f 21 0a 0a                           |Hello!..|
00000008

Having the text input come from a file is a simple change.

1 Like

Ada.Text_IO translates delimiters of a text file and/or file records (in a more advanced file system, e.g. under VMS). As such it meant for text files only. In theory one should use Ada.Text_IO. In practice, dealing with a UNIX clone, an OS that conflates and twists everything, where nothing is a rule and everything a convention, stream I/O is a safer choice because it works at the most low level possible.

1 Like