Intermediate

I/O Operations in Ada

Master input and output in Ada - formatted console output, reading from stdin, file reading and writing, and robust I/O exception handling with GNAT

Input and output are where a program meets the outside world, and Ada treats that boundary with the same seriousness it applies to everything else. Rather than a single grab-bag library, Ada ships a family of strongly typed I/O packages: Ada.Text_IO for human-readable text, Ada.Integer_Text_IO and Ada.Float_Text_IO for numeric formatting, and the same File_Type abstraction for both the terminal and files on disk.

In the Hello World tutorial you saw Put_Line write a string to the console. This tutorial goes much further: precise column-aligned numeric output, reading typed values from standard input, creating and reading files, and - crucially for a safety-critical language - handling the exceptions that I/O can raise. Because I/O failures (a missing file, a bad number, an unexpected end of file) are real-world certainties, Ada surfaces them as named exceptions you are expected to catch.

As a strongly typed language, Ada will not silently coerce a string into a number or let you read past the end of a file. Every read has a type, every file has a mode, and every error has a name. The result is verbose compared to a dynamic language, but it is exactly this explicitness that keeps Ada code running unattended in aircraft and trains for decades.

Formatted Console Output

Put_Line and Put write strings, but real programs need to format numbers into columns and control decimal places. Ada separates these concerns into per-type packages. Ada.Integer_Text_IO gives integers a Width parameter for right-aligned columns, and Ada.Float_Text_IO gives floats Fore (digits before the point), Aft (digits after), and Exp (exponent width, 0 to disable scientific notation).

Create a file named io_basics.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
with Ada.Text_IO;          use Ada.Text_IO;
with Ada.Integer_Text_IO;  use Ada.Integer_Text_IO;
with Ada.Float_Text_IO;    use Ada.Float_Text_IO;

procedure Io_Basics is
   Count : constant Integer := 42;
   Price : constant Float   := 19.95;
begin
   Put_Line ("=== Console Output in Ada ===");

   --  Put writes with no trailing newline; New_Line adds one
   Put ("No newline here... ");
   Put ("still the same line");
   New_Line;

   --  Integer'Image yields a leading space for non-negative values
   Put_Line ("Count is" & Integer'Image (Count));

   --  Ada.Integer_Text_IO.Put gives column-width control
   Put ("Right-aligned in width 6: [");
   Put (Count, Width => 6);
   Put_Line ("]");

   --  Ada.Float_Text_IO.Put with a fixed format (no exponent)
   Put ("Price: ");
   Put (Price, Fore => 1, Aft => 2, Exp => 0);
   New_Line;
end Io_Basics;

Two idioms are worth noting. Integer'Image (Count) is the attribute form: it converts any integer to a String and prepends a space for non-negative values (and a - for negatives), which is why "Count is" & Integer'Image (Count) reads cleanly. The Put from Ada.Integer_Text_IO, by contrast, is overloaded for numeric types and pads to a field width instead of concatenating into a string.

Reading Input from the Terminal

Reading input mirrors writing it. Get_Line reads a whole line of text into a fixed-size String and reports how many characters were actually read through its Last out-parameter - you then slice the buffer with Name (1 .. Last). For numbers, the overloaded Get from Ada.Integer_Text_IO skips leading whitespace and parses a typed value, raising Data_Error if the text is not a valid integer.

Create a file named read_input.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
with Ada.Text_IO;          use Ada.Text_IO;
with Ada.Integer_Text_IO;  use Ada.Integer_Text_IO;

procedure Read_Input is
   Name : String (1 .. 100);
   Last : Natural;
   Age  : Integer;
begin
   Put ("Enter your name: ");
   Get_Line (Name, Last);

   Put ("Enter your age: ");
   Get (Age);

   Put_Line ("Hello, " & Name (1 .. Last) & "!");
   Put ("Next year you will be ");
   Put (Age + 1, Width => 0);
   New_Line;
end Read_Input;

Because this program reads from standard input, run it with input piped in (note the -i flag so Docker keeps stdin open):

1
2
3
printf 'Ada Lovelace\n36\n' | \
  docker run --rm -i -v $(pwd):/app -w /app codearchaeology/ada:latest \
  sh -c 'gnatmake -q read_input.adb && ./read_input'

With the piped input above, the program prints:

Enter your name: Enter your age: Hello, Ada Lovelace!
Next year you will be 37

The two prompts appear back-to-back because piped input is not echoed to the terminal. Passing Width => 0 to the integer Put requests the minimal field width, so 37 appears with no padding.

Writing and Reading Files

Files use the very same Ada.Text_IO package and the same Put_Line/Get_Line subprograms - the only difference is that they take a File_Type value as their first argument. You obtain that value by declaring a File_Type variable and calling Create (to make a new file for writing) or Open (for an existing file). Each file carries a mode: Out_File to write, In_File to read, or Append_File to add to the end. Always Close the file when finished so buffered data is flushed.

Create a file named file_io.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
with Ada.Text_IO;  use Ada.Text_IO;

procedure File_Io is
   Output : File_Type;
   Input  : File_Type;
   Line   : String (1 .. 200);
   Last   : Natural;
begin
   --  Write three lines to a brand-new file
   Create (File => Output, Mode => Out_File, Name => "notes.txt");
   Put_Line (Output, "Ada was named after Ada Lovelace.");
   Put_Line (Output, "It was standardized in 1983.");
   Put_Line (Output, "GNAT is the GNU Ada compiler.");
   Close (Output);

   Put_Line ("Wrote notes.txt successfully.");

   --  Read the file back, one line at a time
   Open (File => Input, Mode => In_File, Name => "notes.txt");

   Put_Line ("--- Contents of notes.txt ---");
   while not End_Of_File (Input) loop
      Get_Line (Input, Line, Last);
      Put_Line (Line (1 .. Last));
   end loop;

   Close (Input);
end File_Io;

The while not End_Of_File (Input) loop pattern is the canonical way to read a text file to its end in Ada. End_Of_File returns True once there is nothing left to read, so the loop stops cleanly without ever reading past the end. This is the program we will run with Docker below, since it produces deterministic output with no external input.

Handling I/O Exceptions

I/O is where the real world intrudes: files go missing, disks fill up, and data arrives malformed. Ada models each of these as a named exception declared in Ada.Text_IO. The most common are Name_Error (the file named in Open does not exist), End_Error (an attempt to read past end of file), Data_Error (text that does not match the expected type), and Use_Error (an operation the file system refuses). A begin ... exception ... end block lets you respond to each by name and keep running.

Create a file named io_exceptions.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
with Ada.Text_IO;  use Ada.Text_IO;

procedure Io_Exceptions is
   Input : File_Type;
   Line  : String (1 .. 200);
   Last  : Natural;
begin
   begin
      Open (File => Input, Mode => In_File, Name => "missing.txt");
      Get_Line (Input, Line, Last);
      Put_Line (Line (1 .. Last));
      Close (Input);
   exception
      when Name_Error =>
         Put_Line ("Error: the file does not exist.");
      when End_Error =>
         Put_Line ("Error: reached the end of the file unexpectedly.");
      when others =>
         Put_Line ("Error: an unexpected I/O problem occurred.");
   end;

   Put_Line ("Program continues after handling the error.");
end Io_Exceptions;

Because missing.txt does not exist, Open raises Name_Error, the matching handler runs, and execution continues past the block - exactly the controlled recovery a long-running system needs. The when others arm is a catch-all safety net for any exception you did not name explicitly. This is Ada’s I/O philosophy in miniature: failures are not crashes to be feared but named events to be handled.

Running with Docker

The file_io.adb example is fully self-contained - it writes a file and reads it back without any terminal input - so it makes the cleanest demonstration:

1
2
3
4
5
6
# Pull the Ada (GNAT) image
docker pull codearchaeology/ada:latest

# Compile and run the file I/O example
docker run --rm -v $(pwd):/app -w /app codearchaeology/ada:latest \
  sh -c 'gnatmake file_io.adb && ./file_io'

The -v $(pwd):/app flag mounts your current directory into the container, so the notes.txt file the program creates appears in your working directory after the run. To try the other examples, swap file_io for io_basics or io_exceptions in the command above.

Expected Output

Running the file_io example produces:

Wrote notes.txt successfully.
--- Contents of notes.txt ---
Ada was named after Ada Lovelace.
It was standardized in 1983.
GNAT is the GNU Ada compiler.

After the run, a notes.txt file containing those three lines remains in your working directory.

Key Concepts

  • One abstraction for console and files - Ada.Text_IO uses the same Put_Line and Get_Line subprograms for both; passing a File_Type first argument is the only difference.
  • Typed numeric I/O lives in its own packages - Ada.Integer_Text_IO and Ada.Float_Text_IO add formatting parameters like Width, Fore, Aft, and Exp for column-aligned, fixed-precision output.
  • Get_Line reports its length - reading into a fixed String returns a Last out-parameter; always slice with (1 .. Last) rather than using the whole buffer.
  • Files have explicit modes - Create with Out_File to write, Open with In_File to read, Append_File to extend; always Close to flush buffered data.
  • End_Of_File drives read loops - while not End_Of_File (F) loop is the idiomatic way to consume a text file to its end without reading past it.
  • I/O errors are named exceptions - Name_Error, End_Error, Data_Error, and Use_Error let you recover precisely, and when others provides a catch-all.
  • 'Image versus formatted Put - Integer'Image returns a String (with a leading space) for concatenation, while the overloaded Put pads numbers into a field width for tabular output.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/ada:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining