Author: R. Koucha
Last update: 28-Mar-2019

SourceForge.net Logo
Support This Project

Programmed Dialogue with Interactive Programs

(PDIP)


Introduction
Usage of the command
Examples with the command
Application programming Interface (API)
Download
Build, installation & tests
Applications based on PDIP
About the author
Related links


Introduction


PDIP stands for Programmed Dialogue with Interactive Programs. It is  a command line oriented utility looking like a simplified  version  of the famous EXPECT utility. The acronym PDIP itself comes from the first lines of the manual of EXPECT.  Like EXPECT,  it  is a freeware and it interprets a scripting language to dialog with an interactive program as a human operator would do. But it has not all the bells and whistles of EXPECT which is able to interact with multiple programs  at the same time, accept a high level scripting language providing branching and high level control structures or giving back the control to the operator during a session.

PDIP accepts a very simple language on the input to provide basic functions such as:

PDIP has been designed to run under any Linux operating system.

The command to launch along with its options and parameters is passed at the end of the command line. If options are passed to pdip and/or the command, then  the  command  must  be preceded  by  a double hyphen (--). Otherwise the options will be passed to pdip and will trigger errors.

For C language programmers, PDIP provides a C language Application Programming Interface (API) to give the ability to pilot processes from an application process.

Usage of the command

The command line to launch PDIP looks like:

pdip [ -s cmdfile ] [ -b buffer-size ] [ -d level ] [ -h ] [ -V ] [ -e ] [ -t ] [ -o ] [ -p ]-- command options parameters...


The options are:

-b buffer-size | --bufsz=buffer-size
Size in bytes of the internal I/O buffer (default: 512).

-d | --debug
Set the debug mode. It is helpful to understand why a command script does not  work as expected.

-h | --help

Display the help of the command (subset of this man page).

-s cmdfile | --script=cmdfile
Script of input commands (default stdin). PDIP reads cmdfile or the standard input (default) and accepts the following commands:

#...
# and the following words up to the end of line are ignored (used for comments)

dbg level
Set the debug level to level.  The higher the level, the more traces you get. The value 0 deactivates the debug mode.

timeout x
Set  to  x  seconds the maximum time to wait on each following commands (the value 0 cancels the timeout, this is the default)

recv "w1 w2..."
Wait for a line with the pattern w1 w2...  from the program. The pattern is  regular expression conforming to regex (cf. Section 7 of Linux manual).

send "w1 w2..."
Send  the string w1 w2...  to the program.

print "w1 w2..."
Print the string w1 w2...  onto the standard output.

For both send and print commands, the string may contain control characters with the notation "^character". For example, "^C" means CONTROL C. The following shortcuts are also accepted:

\a Bell
\b Backspace
\t Horizontal tabulation
\n New line
\v Vertical tabulation
\f Form feed
\r Carriage return
\" Double quote
\\ Backslash
\[ Escape
\] Group Separatorroup Separator
\^ ^ character

sig signame
Send the Linux signal signame to the program.  signame is one of: HUP, INT, QUIT, ILL, TRAP, ABRT, BUS, FPE, KILL, USR1, SEGV, USR2, PIPE, ALRM, TERM.

sleep x

Stop activity during x seconds

exit
Terminate PDIP

sh [-s] cmd par...
Launch the cmd par...  shell command (synchronously if -s is specified).

-V | --version
Display the version of the software.

-e | --error
Redirect error output of the controlled program.

-t | --term
Make pdip behave as a simple line mode terminal.

-o | --outstand
Make pdip print unread data from the controlled program at the end of the session.

-p | --propexit
Propagate  the  exit  code of the controlled program to pdip.  This makes pdip exit with the exit code of the controlled program (the exit code is 1 if the controlled program terminates because of a signal). By default, the exit code of pdip is 0 no matter the exit code of the controlled program  unless there is an error internal to pdip.

-R | --backread
Read and store in background incoming data from controlled program even when no 'recv' command is on track. This avoids the blocking of the controlled program on a 'write' system call because of the saturation of the internal pseudo-terminal buffer when pdip is not in a data reception state (i.e. 'recv' command). But this makes pdip allocate dynamic memory to store the pending read data.


Examples with the command

The  following  example  shows  how  to  set up a telnet connection to a given host called ’remote’ on  the  TCP  port  34770 with the login name ’foo’ and password ’bar’.  Since the remote port is specified with an option (-p), it is mandatory to put a  double  hyphen  (--) before  the command to launch. Commands are injected on the standard  input.  We wait for the ’$’ prompt and launch the ls(1) command before disconnecting from the shell via the exit command.

$ pdip -- telnet -p 34770 remote
recv "login"
send "foo\n"   # Login name is ’foo’
recv "Password"
send "bar\n"   # Password is ’bar’
recv "\$ "     # Inhibition of the metacharacter ’$’ with ’\’
send "ls\n"    # Launch the ’ls’ command
recv "\$ "
send "exit\n"  # Exit from the shell
exit           # Exit from PDIP
$

The following example shows how to set up a ftp  connection  to  a  given  host  called ’remote’ with the login name ’foo’ and password ’bar’.  Commands are injected on the standard input. We wait for the ’ftp>’ prompt at the beginning of  the  line  and  launch  the ’help’ command before disconnecting from ftp with the ’quit’ command.

$ pdip ftp remote
recv "Name"
send "foo\n"    # Login name is ’foo’
recv "Password"
send "bar\n"    # Password is ’bar’
recv "^ftp> "   # Prompt at beginning of line
send "help\n"   # Launch the ’help’ command
recv "^ftp> "
send "quit\n"   # Terminate FTP
exit            # Exit from PDIP
$

The  following example shows how to interact with the program bc which does not display any prompt. We use the metacharacter ’$’ to synchronize on end of  lines.  Two  operations are launched ’3+4’ and ’6*8’. Then we quit bc.

$ pdip bc
recv "warranty"  # Near the end of the startup banner
recv "$"         # End of last line of the banner
send "3+4\n"
recv "$"         # Receive the end of line of the echo
recv "$"         # Receive the end of line of the result
send "6*8\n"
recv "$"
recv "$"
send "quit\n"   # Terminate BC
exit            # Exit from PDIP
$

The following example shows how to set up a telnet(1) connection  to  a  given  host  called ’remote’  with  the login name ’foo’ and password ’bar’.  Commands are injected on the standard  input.  With  a  regular  expression,  we  wait   for   the   prompt   of   the   form "xxxx-<login_name>-pathname>  "  or  "xxxx-<login_name>-pathname>"  at  the beginning of the line.  Then we launch the ’ls -l’ command  before  disconnecting  from  telnet(1)  with  the ’exit’ command.

$ pdip telnet remote
recv "login:"
send "foo\n"                  # Login name is ’foo’
recv "Password:"
send "bar\n"                  # Password is ’bar’
recv "^(.)+-foo-(.)+(>|> )$"  # Prompt at beginning of line
send "ls -l\n"                # Launch the ’ls -l’ command
recv "^(.)+-foo-(.)+(>|> )$"
send "exit\n"                 # Terminate telnet
exit                          # Exit from PDIP
$



Application Programming Interface (API)


Synopsis
Description
Return value
Errors
Mutual exclusion
Examples

Synopsis

#include "pdip.h"

int pdip_configure(int sig_hdl_internal, int debug_level);

int pdip_cfg_init(pdip_cfg_t *cfg);
pdip_t pdip_new(pdip_cfg_t *cfg);
int pdip_delete(pdip_t ctx, int *status);

int pdip_exec(pdip_t ctx, int ac, char *av[]);
int pdip_fd(pdip_t ctx);

int pdip_set_debug_level(pdip_t ctx, int level);

int pdip_send(pdip_t ctx, const char *format, ...);
int pdip_recv(pdip_t *ctx, const char *regular_expr, char **display, size_t *display_sz, size_t *data_sz, struct timeval *timeout);
int pdip_sig(pdip_t ctx, int sig);
int pdip_flush(pdip_t ctx, char **display, size_t *display_sz, size_t *data_sz);
int pdip_status(pdip_t ctx, int *status, int blocking);

int pdip_lib_initialize(void);

Description

The PDIP API is a C language library named libpdip.so providing the ability to control terminal oriented interactive processes by simulating an operator. Each controlled process is associated to a PDIP object.

The transition diagram of a PDIP object as well as the services applicable to each state are depicted below:

state diagram

pdip_configure() is supposed to be called prior any other service as it configures some internals of the library:


If a configuration structure is passed to pdip_new(), it is advised to call pdip_cfg_init() which initializes the cfg configuration structure to its default values. Using this call makes any future evolutions to the structure transparent to the applications designed with older versions of the library. The structure is described below in the presentation of pdip_new().

pdip_new() creates a PDIP object. To configure the object, the function is passed an optional parameter cfg of type:

typedef struct
{
  FILE *dbg_output;  // Stream into which are displayed the debug messages
                     // of the PDIP object
                     // If NULL, it defaults to stderr
  FILE *err_output;  // Stream into which are displayed the error messages
                     // of the PDIP object
                     // If NULL, it defaults to stderr

  int debug_level;   // Debug level of the PDIP object. The higher the value, the more debug
                     // messages are displayed
                     // Default: 0 (no debug messages)

  unsigned int flags;
#define PDIP_FLAG_ERR_REDIRECT 0x01 // If set, the stderr of the controlled process is also
                                    // redirected to the main program.
                                    // Otherwise, it is inherited from the main program (default)

#define PDIP_FLAG_RECV_ON_THE_FLOW 0x02 // If set, data are returned to the user as
                                        // they are received even if the regular expression
                                        // is not found (returned code is PDIP_RECV_DATA)
                                        // Otherwise, the data is returned when
                                        // the regular expression is found (default)

  unsigned char *cpu; // Array of bits describing the CPU affinity of the controlled process
                      // Allocated/freed with pdip_cpu_alloc()/pdip_cpu_free()
                      // cf. pdip_cpu(3)
                      //By default, the affinity is inherited from the main program

  size_t buf_resize_increment; // Amount of space in bytes to add to the reception
                               // buffer each time additional space is needed
                               // Default is 1 KB
} pdip_cfg_t;

If this parameter is NULL, the default values are used. If it is not NULL, it is advised to initialize it with a call to pdip_cfg_init() before setting its fields. The function returns a PDIP object of type pdip_t. This object is passed as parameter to most of the following services.

pdip_delete() is the counterpart of pdip_new() as it deallocates the ctx PDIP object allocated by a preceding call to pdip_new(). If not NULL, status is updated with the termination status of the controlled process.

pdip_exec() executes a program which will be controlled by the ctx PDIP object. ac and av respectively describe the number of parameters and the parameters of the program to execute. They behave the same as the famous parameters passed to the main() function of the C language programs. In other words, they describe the program name to run along with its parameters.

pdip_fd() returns the file descriptor of the ctx PDIP object (that is to say the file descriptor of the internal pseudo-terminal interfaced with the controlled program). This is useful in event driven applications where it is needed to be warned when data are available from the controlled program.

pdip_set_debug_level() sets the debug level of the ctx PDIP object to the value of level. The higher the value of level, the more debug messages will be displayed. The debug level and the output stream for the debug messages can also be configured at object creation time through the parameter passed to pdip_new(). If ctx is NULL, the service sets the global debug level of the service. That is to say, this controls the debug messages not linked to PDIP objects. A debug level equal to 0 deactivates the display of debug messages.

pdip_send() sends a string formatted with format to the process controlled by the ctx PDIP object. The behaviour of the format is compliant with printf(3). The size of the internal buffer to format the string is 4096 bytes. Over this limit, the function returns an error.

pdip_recv() receives data from the process controlled by the ctx PDIP object. The user may pass a regular expression (regular_expr) compliant with regex(7) and/or a timeout. The data reception stops once either the timeout is elapsed or the regular expression is found. If neither the regular expression is found (or regular_expr is NULL) nor the timeout is set (i.e. timeout is NULL), the function waits indefinitely unless the controlled process dies. The function is passed three additional parameters display, display_sz and data_sz which respectively contain the address of a dynamic memory buffer to store the data in, the physical size of the buffer and the number of bytes that the service stored into it. The data is NUL terminated by the service. data_sz does not count this last character (this is equivallent to the result of a call to strlen(3) on the buffer). If the display address of the buffer is NULL or the display_sz physical size of the buffer is not sufficient to contain the received data, the buffer is allocated or reallocated (in the latter case, the former buffer is freed by the service). Anyway, display and display_sz are respectively updated with the new address and the new size of the buffer. If the user puts a non NULL address in display parameter, IT IS VERY IMPORTANT TO PASS THE BEGINNING ADDRESS OF A DYNAMIC MEMORY BUFFER as the service may reallocate or free it through calls to realloc(3) or free(3).
The timeout is a structure defined in <sys/time.h> as:

struct timeval
{
  long tv_sec;     // Seconds
  long tv_usec;   // Microseconds
};

pdip_sig() sends the sig Linux signal to the process controlled by the ctx PDIP object. The list of available signals is defined in <signal.h>.

pdip_flush() flushes any outstanding data from the ctx PDIP object. In other words, it concerns any data coming from the controlled process which have not been received yet by the application through a call to pdip_recv() service. The function behaves the same as pdip_recv() to update its display, display_sz and data_sz parameters.

pdip_status() returns the exit status in status (if not NULL) of the dead controlled process attached to the ctx PDIP object. The value of status can be inspected with macros as explained in wait(2). If the third parameter blocking is set to non 0, the service waits until the controlled process terminates. If set to 0, the service returns immediately reporting an error (errno is set to EAGAIN) if the controlled process is not terminated or 0 if the process is terminated.

pdip_lib_initialize() is to be called in child processes using the PDIP API. By default, PDIP API is deactivated upon fork(2).

Return value

pdip_new() returns a PDIP object of type pdip_t if there are no error or (pdip_t)0 upon error (errno is set).

pdip_exec() returns the pid of the controlled process or -1 upon error (errno is set).

pdip_fd() returns the file descriptor of the pseudo-terminal linked with the controlled process or -1 upon error (errno is set).

pdip_cfg_init(), pdip_configure(), pdip_delete(), pdip_set_debug_level(), pdip_flush(), pdip_sig(), pdip_status() and pdip_lib_initialize() return 0 when there are no error or -1 upon error (errno is set).

pdip_send() returns the amount of sent characters or -1 upon error (errno is set).

pdip_recv() returns:

Errors

The functions may set errno with the following values:

Mutual exclusion

By default, the service supposes that each PDIP object is managed at most by one thread. If an application shares a PDIP object between multiple threads, it is responsible to manage the mutual exclusion on its side.

Examples

In the related links paragraph, there are references to ISYS and RSYS packages which are applications using PDIP library to propose alternate versions of system(3) of the C library.

The following program controls a bash shell. Before running the shell, the program modifies the PS1 environment variable to define the prompt displayed by the shell otherwise the prompt would be inherited by the current shell which may be anything defined by the operator. Then, it synchronizes on the display of the first prompt, executes the ls -la / command and synchronizes on the prompt appearing right after the command execution. Then it executes the exit command to stop the shell.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include "pdip.h"



int main(int ac, char *av[])
{
pdip_t pdip;
char *bash_av[4];
int rc;
char *display;
size_t display_sz;
size_t data_sz;
pdip_cfg_t cfg;
int status;

  (void)ac;
  (void)av;

  // Let the service manage the SIGCHLD signal as we don't fork/exec any
  // other program
  rc = pdip_configure(1, 0);
  if (rc != 0)
  {
    fprintf(stderr, "pdip_configure(): '%m' (%d)\n", errno);
    return 1;
  }

  // Create a PDIP object
  pdip_cfg_init(&cfg);
  // The bash prompt is displayed on stderr. So, to synchronize on it, we must
  // redirect stderr to the PTY between PDIP and bash
  cfg.flags |= PDIP_FLAG_ERR_REDIRECT;
  cfg.debug_level = 0;
  pdip = pdip_new(&cfg);
  if (!pdip)
  {
    fprintf(stderr, "pdip_new(): '%m' (%d)\n", errno);
    return 1;
  }

  // Export the prompt of the BASH shell
  rc = setenv("PS1", "PROMPT> ", 1);
  if (rc != 0)
  {
    fprintf(stderr, "setenv(PS1): '%m' (%d)\n", errno);
    return 1;
  }

  // Attach a bash shell to the PDIP object
  bash_av[0] = "/bin/bash";
  bash_av[1] = "--noprofile";
  bash_av[2] = "--norc";
  bash_av[3] = (char *)0;
  rc = pdip_exec(pdip, 3, bash_av);
  if (rc != 0)
  {
    fprintf(stderr, "pdip_exec(bash): '%m' (%d)\n", errno);
    return 1;
  }

  // Synchronize on the first displayed prompt
  display = (char *)0;
  display_sz = 0;
  data_sz = 0;
  rc = pdip_recv(pdip, "^PROMPT> ", &display, &display_sz, &data_sz, (struct timeval*)0);
  if (rc != PDIP_RECV_FOUND)
  {
    fprintf(stderr, "pdip_recv(): Unexpected return code %d\n", rc);
    return 1;
  }

  // Display the result
  printf("%s", display);

  // Execute the "ls -la /" command
  rc = pdip_send(pdip, "ls -la /\n");
  if (rc < 0)
  {
    fprintf(stderr, "pdip_send(ls -la /): '%m' (%d)\n", errno);
    return 1;
  }

  // Synchronize on the prompt displayed right after the command execution
  // We pass the same buffer that will be eventually reallocated
  rc = pdip_recv(pdip, "^PROMPT> ", &display, &display_sz, &data_sz, (struct timeval*)0);
  if (rc != PDIP_RECV_FOUND)
  {
    fprintf(stderr, "pdip_recv(): Unexpected return code %d\n", rc);
    return 1;
  }

  // Display the result
  printf("%s", display);

  // Execute "exit" to go out of the shell
  rc = pdip_send(pdip, "exit\n");
  if (rc < 0)
  {
    fprintf(stderr, "pdip_send(exit): '%m' (%d)\n", errno);
    return 1;
  }

  // Wait for the end of "bash"
  rc = pdip_status(pdip, &status, 1);
  if (0 != rc)
  {
    fprintf(stderr, "pdip_status(): '%m' (%d)0, errno);
    return 1;
  }

  printf("bash ended with status 0x%x0, status);

  // Delete the PDIP object
  rc = pdip_delete(pdip, 0);
  if (rc != 0)
  {
    fprintf(stderr, "pdip_delete(): '%m' (%d)\n", errno);
    return 1;
  }

  return 0;

} // main

The program execution displays:

$ ./man_exe_1
PROMPT> ls -la /
total 108
drwxr-xr-x 24 root root 4096 oct. 22 21:28 .
drwxr-xr-x 24 root root 4096 oct. 22 21:28 ..
drwxr-xr-x 2 root root 4096 juil. 6 17:22 bin
drwxr-xr-x 4 root root 4096 oct. 22 21:42 boot
drwxrwxr-x 2 root root 4096 janv. 4 2017 cdrom
[...]
lrwxrwxrwx 1 root root 29 oct. 22 21:28 vmlinuz -> boot/vmlinuz-4.8.0-59-generic
lrwxrwxrwx 1 root root 29 juil. 6 17:24 vmlinuz.old -> boot/vmlinuz-4.8.0-58-generic
PROMPT> bash ended with status 0x0

The following program is passed a mathematic operation as argument. It uses bc tool to compute it. Some precautions are required for the synchronization as bc does not display any prompt. Hence, the program uses "end of line" pattern matching.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <libgen.h>
#include "pdip.h"

int main(int ac, char *av[])
{
pdip_t pdip;
char *bash_av[3];
int rc;
char *display;
size_t display_sz;
size_t data_sz;
pdip_cfg_t cfg;
char *op;
int i;
int status;

   if (ac != 2)
   {
     fprintf(stderr, "Usage: %s operation\n", basename(av[0]));
     return 1;
   }

   // Let the service manage the SIGCHLD signal as we don't fork/exec any
   // other program
   rc = pdip_configure(1, 0);
   if (rc != 0)
   {
     fprintf(stderr, "pdip_configure(): '%m' (%d)\n", errno);
     return 1;
   }

   op = av[1];

   // Create a PDIP object
   pdip_cfg_init(&cfg);
   cfg.debug_level = 0;
   pdip = pdip_new(&cfg);
   if (!pdip)
   {
     fprintf(stderr, "pdip_new(): '%m' (%d)\n", errno);
     return 1;
   }

   // Attach the "bc" command to the PDIP object
   // Option "-q" launches "bc" in quiet mode: it does not display
   // the welcome banner
   bash_av[0] = "bc";
   bash_av[1] = "-q";
   bash_av[2] = (char *)0;
   rc = pdip_exec(pdip, 2, bash_av);
   if (rc != 0)
   {
     fprintf(stderr, "pdip_exec(bc -q): '%m' (%d)\n", errno);
     return 1;
   }

   // Execute the operation
   rc = pdip_send(pdip, "%s\n", op);
   if (rc < 0)
   {
     fprintf(stderr, "pdip_send(op): '%m' (%d)\n", errno);
     return 1;
   }

   // Initialize the display buffer
   display = (char *)0;
   display_sz = 0;
   data_sz = 0;

   // For some reasons, "bc" echoes the operation two times ?!?
   // ==> Skip them
   for (i = 0; i < 2; i ++)
   {
     // Synchronize on the echo
     // We pass the same buffer that will be eventually reallocated
     rc = pdip_recv(pdip, "^.+$", &display, &display_sz, &data_sz, (struct timeval*)0);
     if (rc != PDIP_RECV_FOUND)
     {
       fprintf(stderr, "pdip_recv(): Unexpected return code %d\n", rc);
       return 1;
     }

     // Print the operation on the screen (one time :-)
     if (0 == i)
     {
       printf("%s=", display);
     }

     // Skip the end of line
     rc = pdip_recv(pdip, "$", &display, &display_sz, &data_sz, (struct timeval*)0);
     if (rc != PDIP_RECV_FOUND)
     {
       fprintf(stderr, "pdip_recv($): Unexpected return code %d\n", rc);
       return 1;
     }
   } // End for

   // Synchronize on the result of the operation
   rc = pdip_recv(pdip, "^.+$", &display, &display_sz, &data_sz, (struct timeval*)0);
   if (rc != PDIP_RECV_FOUND)
   {
     fprintf(stderr, "pdip_recv(): Unexpected return code %d\n", rc);
     return 1;
   }

   // Display the result of the operation with '0 as the match
   // does not embed the end of line
   printf("%s0, display);
   fflush(stdout);

   // Skip the end of line
   rc = pdip_recv(pdip, "$", &display, &display_sz, &data_sz, (struct timeval*)0);
   if (rc != PDIP_RECV_FOUND)
   {
     fprintf(stderr, "pdip_recv($): Unexpected return code %d\n", rc);
     return 1;
   }

   // Execute "quit" to go out
   rc = pdip_send(pdip, "quit\n");
   if (rc < 0)
   {
     fprintf(stderr, "pdip_send(quit): '%m' (%d)\n", errno);
     return 1;
   }

   // Synchronize on the echo of "quit"
   // We pass the same buffer that will be eventually reallocated
   rc = pdip_recv(pdip, "^quit$", &display, &display_sz, &data_sz, (struct timeval*)0);
   if (rc != PDIP_RECV_FOUND)
   {
     fprintf(stderr, "pdip_recv(): Unexpected return code %d\n", rc);
     return 1;
   }

   // Wait for the end of "bc"
   rc = pdip_status(pdip, &status, 1);
   if (0 != rc)
   {
     fprintf(stderr, "pdip_status(): '%m' (%d)0, errno);
     return 1;
   }

   printf("bc ended with status 0x%x0, status);

   // Delete the PDIP object
   rc = pdip_delete(pdip, 0);
   if (rc != 0)
   {
     fprintf(stderr, "pdip_delete(): '%m' (%d)\n", errno);
     return 1;
   }

   return 0;

} // main

The program execution for "67*18" displays:

$ ./man_exe_2 67*18
67*18=1206
bc ended with status 0x0



Download


PDIP can be downloaded from this page in one of the following three ways:

Build, installation & tests

PDIP comes with two build suites: cmake and autotools.

All information about build, test, installation are given in the README text file located at the top level of the source tree of the package.


Applications based on PDIP

This document presents a way to optimize the C library's system() service with two solutions called isys and rsys which are LGPL open sources based on PDIP API.


About the author

The author is an engineer in computer sciences located in France. He is glad to graciously offer this simple utility under the GPL open source license. He can be contacted at "rachid dot koucha at gmail dot com" or you can have a look at his WEB home page.


Related links