Gem #54: Scripting Capabilities in GNAT (Part 2)

Let's get started…


A lot of applications have a need to execute commands on the machine. There are several use cases: a program might wish to wait for a command to complete (for instance the command generates a file that the application then needs to parse); it might spawn a command in the background and continue to execute in the meantime; or it might need to interact with an external application (sending data to its input stream and reading its output).

Spawning a process that the application doesn't need to interact with is easy. The GNAT run-time provides a package GNAT.OS_Lib containing a number of subprograms that are low-level interfaces to the system. In particular, the package provides several subprograms with names Spawn and Non_Blocking_Spawn which, as their name indicates, will spawn an external application, and wait or not for its completion.

with GNAT.OS_Lib;  use GNAT.OS_Lib;

procedure Main is
   Command : constant String := "myapp -f 'a string with spaces'";
   --  We assume the slightly more complex case where the arguments of the
   --  command are part of the same string (this is generally the case when
   --  the command is provided interactively by the user).

   Args        : Argument_List_Access;
   Exit_Status : Integer;
   Pid         : Process_Id;
   Success     : Boolean;

begin
   --  Prepare the arguments. Splitting properly takes quotes into account.

   Args := Argument_String_To_List (Command);

   --  Spawn the command and wait for its possible completion

   if Background_Mode then
      Pid := Non_Blocking_Spawn
         (Program_Name => Args (Args'First).all,
          Args         => Args (Args'First + 1 .. Args'Last));

      --  We could also wait for completion:
      --  Wait_Process (Pid, Success);

   else
      Exit_Status := Spawn
         (Program_Name => Args (Args'First).all,
          Args         => Args (Args'First + 1 .. Args'Last));
   end if;

   --  Free memory
   
   Free (Args);

end Main;

While discussing GNAT.OS_Lib, it is worth investigating its remaining subprograms. They provide system-independent file manipulation (checking whether a file exists, whether it is a directory or a symbolic link, access to the date of the last modification for the file, copying, deleting, and renaming files, etc.). One very interesting subprogram is Normalize_Pathname, which is able to resolve symbolic links and convert the casing of file names depending on whether they can be accessed using a unique name or with case insensitivity. This provides a convenient way to check whether two file names are the same (handling "..", symbolic links, and case resolution is quite complicated to get right and system-independent, and GNAT.OS_Lib takes care of it for you).

Among the three use cases we mentioned at the beginning, the most complicated one is where the application needs to interact with a process. Such interaction is provided through the GNAT.Expect package. Here, you can spawn a process, and, while it is running, send it input and read its output. Communication between the two processes is done through pipes. Another implementation, using an approach based on ttys (pseudo terminals), is planned to be made available as part of the GNAT Reusable Components, and is designed to provide a closer emulation of what happens when you spawn a process from a terminal.

Here is an example of using GNAT.Expect:

with GNAT.Expect;  use GNAT.Expect;

procedure Main is
   Command : constant String := "gdb myapp"; --  Let's spawn a debugger session.
   Pd      : Process_Descriptor;
   Args    : Argument_List_Access;
   Result  : Expect_Match;

begin
   --  First we spawn the process. Splitting the program name and the
   --  arguments is done as in the previous example, using
   --  Argument_String_To_List. Remember to free the memory at the end.

   Args := Argument_String_To_List (Command);

   Non_Blocking_Spawn
      (Pd,
       Command     => Args (Args'First).all,
       Args        => Args (Args'First + 1 .. Args'Last),
       Buffer_Size => 0);
   
   --  The debugger is now running. Let's send a command.

   Send (Pd, "break 10");

   --  Then let's read the output of the debugger.
   --  Here, we are expecting any possible non-empty output (hence the
   --  ".+" regexp). We might in fact be expecting a file name that
   --  gdb uses to confirm a breakpoint. The regexp would be something like:
   --   "file (.*), line (\d+)"

   Expect (Pd, Result, Regexp => ".+", Timeout => 1_000);

   case Result is
      when Expect_Timeout => ...;  --  gdb never replied
      when 1  => ...;              --  the regexp matched
          --  We then have access to all the output of gdb since the
          --  last call to Expect, through the Expect_Out subprogram.
   end case;

   --  When we are done, terminate gdb:

   Close (Pd);      
end Main;

This example really only briefly touched on the capabilities of GNAT.Expect. As a side note, the whole of GPS, our integrated development environment uses this package to interact with external processes.