Difference between revisions of "Executing External Programs/es"

From Free Pascal wiki
Jump to navigationJump to search
Line 223: Line 223:
  
 
   Ambas unidades son bastante independientes del resto de fuentes de pasdoc, así que pueden servir como ejemplos reales de la utilización de TProcess para ejecutar y comunicarse a través de tuberías con otros programas.
 
   Ambas unidades son bastante independientes del resto de fuentes de pasdoc, así que pueden servir como ejemplos reales de la utilización de TProcess para ejecutar y comunicarse a través de tuberías con otros programas.
 +
 +
=== Reemplazar operadores de ''Shell'' como  "|",  "<" y  ">" ===
 +
 +
&nbsp;&nbsp;&nbsp;A veces queremos ejecutar una orden compleja que envía (entuba) sus datos a otra orden o a un archivo. Algo como
 +
 +
  ShellExecute('primeraorden.exe | segundaorden.exe');
 +
o
 +
  ShellExecute('dir > salida.txt');
 +
 +
&nbsp;&nbsp;&nbsp; Esto no funciona con TProcess. p.e.:
 +
  // esto no funciona
 +
  Process.CommandLine := 'primeraorden.exe | segundaorden.exe';
 +
  Process.Execute;
 +
 +
==== ¿Por qué utilizar operadores especiales para redirigir la salida no funciona? ====
 +
&nbsp;&nbsp;&nbsp;TProcess es justamente, eso, un proceso, no es un entorno de ''shell''. No es dos procesos, únicamente es un proceso. Es posible, no obstante, redirigir la salida de la manera que queremos. Ver [[Executing_External_Programs/es#Cómo_redirigir_la_salida_con_TProcess|más adelante, la siguiente sección]].
 +
 +
=== Cómo redirigir la salida con TProcess ===
 +
 +
You can redirect the output of a command to another command by using a TProcess instance for '''each''' command.
 +
 +
Here's an example that explains how to redirect the output of one process to another. To redirect the output of a process to a file/stream see the example [[Executing_External_Programs#Reading_large_output | Reading Large Output ]]
 +
<delphi>
 +
  program Project1;
 +
 
 +
  uses
 +
    Classes, sysutils, process;
 +
 
 +
  var
 +
    FirstProcess,
 +
    SecondProcess: TProcess;
 +
    Buffer: array[0..127] of char;
 +
    ReadCount: Integer;
 +
    ReadSize: Integer;
 +
  begin
 +
    FirstProcess  := TProcess.Create(nil);
 +
    SecondProcess := TProcess.Create(nil);
 +
 
 +
    FirstProcess.Options  := [poUsePipes];
 +
    SecondProcess.Options := [poUsePipes,poStderrToOutPut];
 +
 
 +
    FirstProcess.CommandLine  := 'pwd';
 +
    SecondProcess.CommandLine := 'grep '+ DirectorySeparator+ ' -';   
 +
    // this would be the same as "pwd | grep / -"
 +
 
 +
    FirstProcess.Execute;
 +
    SecondProcess.Execute;
 +
 
 +
    while FirstProcess.Running or (FirstProcess.Output.NumBytesAvailable > 0) do
 +
    begin
 +
      if FirstProcess.Output.NumBytesAvailable > 0 then
 +
      begin
 +
        // make sure that we don't read more data than we have allocated
 +
        // in the buffer
 +
        ReadSize := FirstProcess.Output.NumBytesAvailable;
 +
        if ReadSize > SizeOf(Buffer) then
 +
          ReadSize := SizeOf(Buffer);
 +
        // now read the output into the buffer
 +
        ReadCount := FirstProcess.Output.Read(Buffer[0], ReadSize);
 +
        // and write the buffer to the second process
 +
        SecondProcess.Input.Write(Buffer[0], ReadCount);
 +
 
 +
        // if SecondProcess writes much data to it's Output then
 +
        // we should read that data here to prevent a deadlock
 +
        // see the previous example "Reading Large Output"
 +
      end;
 +
    end;
 +
    // Close the input on the SecondProcess
 +
    // so it finishes processing it's data
 +
    SecondProcess.CloseInput;
 +
 
 +
    // and wait for it to complete
 +
    // be carefull what command you run because it may not exit when
 +
    // it's input is closed and the following line may loop forever
 +
    while SecondProcess.Running do
 +
      Sleep(1);
 +
    // that's it! the rest of the program is just so the example
 +
    // is a little 'useful'
 +
 
 +
    // we will reuse Buffer to output the SecondProcess's
 +
    // output to *this* programs stdout
 +
    WriteLn('Grep output Start:');
 +
    ReadSize := SecondProcess.Output.NumBytesAvailable;
 +
    if ReadSize > SizeOf(Buffer) then
 +
      ReadSize := SizeOf(Buffer);
 +
    if ReadSize > 0 then
 +
    begin
 +
      ReadCount := SecondProcess.Output.Read(Buffer, ReadSize);
 +
      WriteLn(Copy(Buffer,0, ReadCount));
 +
    end
 +
    else
 +
      WriteLn('grep did not find what we searched for. ', SecondProcess.ExitStatus);
 +
    WriteLn('Grep output Finish:');
 +
 
 +
    // free our process objects
 +
    FirstProcess.Free;
 +
    SecondProcess.Free;
 +
  end.
 +
</delphi>
 +
 +
That's it. Now you can redirect output from one program to another.
 +
==== Notes ====
 +
This example may seem overdone since it's possible to run "complicated" commands using a shell with TProcess like:
 +
  Process.Commandline := 'sh -c "pwd | grep / -"';
 +
But our example is more crossplatform since it needs no modification to run on Windows or Linux etc. "sh" may or may not exist on your platform and is generally only available on *nix platforms. Also we have more flexibility in our example since you can read and write from/to the input, output and stderr of each process individually, which could be very advantageous for your project.

Revision as of 20:18, 28 December 2010

Deutsch (de) English (en) español (es) français (fr) italiano (it) 日本語 (ja) Nederlands (nl) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

Introducción

   Hay varias formas de ejecutar programas externos, pero nos centraremos en una, TProcess.

Si en Delphi utilizabas siempre ShellExecute y/o WinExec, puedes empezar a usar TProcess como alternativa en FPC/Lazarus (es válido también si estás ejecutando Lazarus en Linux, porque TProcess es multiplataforma).

   Nota: FPC/Lazarus puede utilizar ShellExecute y/o WinExec, pero sólo en Win32. Si tu programa está enfocado a varias plataformas, utiliza TProcess, ¡es mejor!.

TProcess

   Puedes utilizar TProcess para ejecutar programas externos. Algunas de sus ventajas son:

  • Independiente de la plataforma
  • Capaz de leer desde la entrada estándar (stdout) y de escribir en la salida estándar (stdin).

   Nota: TProcess no es un terminal de shell! No puedes ejecutar directamente secuencias de comandos o reorientar la salida usando operadores como "|", ">", "<", "&", etc. Es posible obtener los mismos resultados con TProcess utilizando Pascal, mira algunos ejemplos más abajo..

   Importante: Debes especificar la ruta completa al ejecutable. Por ejemplo "/bin/cp " en lugar de "cp". Si el programa está en la ruta estándar, puedes utilizar la función FindDefaultExecutablePath de la unidad FileUtil de la LCL.

Un ejemplo sencillo

<delphi> // Este es un programa de demostración que enseña como lanzar

// un programa externo.
program lanzarprograma;

uses 
  Classes, SysUtils, Process;

// Definición de "UnProceso" como una variable 
// de tipo "TProcess"
var 
  UnProceso: TProcess;
begin
  // Ahora creamos UnProceso.
  UnProceso := TProcess.Create(nil);

  // Asignamos a UnProceso la orden que debe ejecutar.
  // Vamos a lanzar el compilador de FreePascal 
  UnProceso.CommandLine := 'ppc386 -h';

  // Definimos una opción de comportamiento de 'TProccess'
  // La opción poWaitOnExit hará que nuestro programa
  // se detenga hasta que termine el programa lanzado
  UnProceso.Options := UnProceso.Options + [poWaitOnExit];

  // Lanzamos la ejecución de 'ppc386 -h'.
  UnProceso.Execute;

  // Nuestro programa se detiene hasta que 'ppc386' finaliza.
  UnProceso.Free;   
end.</delphi>

   ¡Eso es todo! Acabas de aprender cómo se ejecuta un programa externo desde dentro de tu propio programa.

Un ejemplo mejorado

   De acuerdo, pero ¿cómo leo la salida que produce un programa que he ejecutado?

   Bien, vamos a ampliar un poco nuestro ejemplo mediante lo siguiente:

<delphi> program lanzarprograma2;

uses 
  Classes, SysUtils, Process;

// Definición de "UnProceso" como una variable de tipo "TProcess"
// También añadimos un TStringList para almacenar los datos 
// leídos desde la salida del programa.
var 
  UnProceso: TProcess;
  UnaLista: TStringList;

// Aquí comienza a ejecutarse nuestro programa.
begin

  // Creamos el objeto TStringList.
  UnaLista := TStringList.Create;

  // Ahora creamos UnProceso.
  UnProceso := TProcess.Create(nil);

  // Asignamos a UnProceso la orden que debe ejecutar.
  // Vamos a lanzazar el compilador de FreePascal 
  UnProceso.CommandLine := 'ppc386 -h';

  // Definimos una opción de comportamiento de 'TProccess'
  // La opción poWaitOnExit hará que nuestro programa
  // se detenga hasta que termine el programa lanzado
  // con la opción poUsePipes    advertimos que vamos a utilizar tuberías de Entrada/Salida                                        
  // para que se capture la salida y así poder recuperar su contenido desde nuestro programa
  UnProceso.Options := UnProceso.Options + [poWaitOnExit, poUsePipes];
  // Lanzamos la ejecución de 'ppc386 -h'.
  UnProceso.Execute;

  // Nuestro programa se detiene hasta que 'ppc386' finaliza.

  // Ahora leemos la salida del programa que acabamos de ejecutar
  // dentro de TStringList.
  UnaLista.LoadFromStream(UnProceso.Output);
  
  // Guardamos la salida en un archivo.
  UnaLista.SaveToFile('salida.txt');

  // Ahora que hemos guardado el archivo podemos liberar la memoria
  // TStringList y TProcess.
  UnaLista.Free;
  UnProceso.Free;   
end.</delphi>

Lectura de muchos datos de salida

   En el ejemplo anterior esperamos a que el programa termine. En ese momento leemos lo que el programa ha escrito en la salida. Pero si el programa escribe muchos datos, la tubería se llena y será necesario leerlos para vaciarla, pero el programa llamante no leerá hasta que el programa llamado haya terminado. Nos encontraremos ante un típico bloqueo de la muerte.

   El siguiente ejemplo, por tanto, no utiliza poWaitOnExit, sino que va leyendo la salida mientras el programa llamado se está ejecutando. La salida se almacena en una memoria intermedia, que se utilizará más tarde para leer en un TStringList, por ejemplo.

<delphi> program procoutlarge;

{   Copyright (c) 2004 by Marc Weustink

    Este ejemplo se ha creado con la esperanza de que resulte útil,
    pero SIN NINGUNA GARANTÍA; ni tan siquiera la garantía implícita de
    COMERCIALIZACIÓN o ADECUACIÓN A UN OBJETIVO PARTICULAR.
}

uses
  Classes, Process, SysUtils;

const
  READ_BYTES = 2048;
  
var
  S: TStringList;
  M: TMemoryStream;
  P: TProcess;
  n: LongInt;
  BytesRead: LongInt;

begin
  // No podemos utilizar poWaitOnExit porque no
  // conocemos el tamaño de la salida. En Linux el tamaño
  // de la salida de la tubería es de 2 kB. Si los datos de salida son más 
  // necesitamos leerlos. Esto no es posible, ya que estamos
  // esperando. Así que aquí nos encontramos en un punto muerto.
  //
  // Se utiliza un Memorystream temporal para almacenar la salida
  
  M := TMemoryStream.Create;
  BytesRead := 0;

  P := TProcess.Create(nil);
  P.CommandLine := 'ppc386 -va bogus.pp';
  P.Options := [poUsePipes];
  WriteLn('-- ejecutando --');
  P.Execute;
  while P.Running do
  begin          
    // nos aseguramos de tener espacio
    M.SetSize(BytesRead + READ_BYTES);
    
    // intentamos leerlo
    n := P.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
    if n > 0 
    then begin
      Inc(BytesRead, n);
      Write('.')
    end
    else begin     
      // sin datos, esperamos 100 milisegundos
      Sleep(100); 
    end;
  end;
  // leemos la última parte
  repeat
    // nos aseguramos de tener espacio
    M.SetSize(BytesRead + READ_BYTES);
    // intentamos leerlo
    n := P.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
    if n > 0 
    then begin
      Inc(BytesRead, n);
      Write('.');
    end;
  until n <= 0;
  if BytesRead > 0 then WriteLn;
  M.SetSize(BytesRead); 
  WriteLn('-- ejecutado --');
  
  S := TStringList.Create;
  S.LoadFromStream(M);
  WriteLn('-- total de líneas = ', S.Count, ' --');
  for n := 0 to S.Count - 1 do
  begin
    WriteLn('| ', S[n]);
  end;
  WriteLn('-- fin --');
  S.Free;
  P.Free;
  M.Free;
end.</delphi>

Notas sobre la utilización de TProcess

   Si estas creando un programa que funciones en diversas plataformas, puedes cambiar el programa a ejecutar de acuerdo con el sistema operativo, mediante las directivas {$IFDEF} y {$ENDIF}.

   Ejemplo: <delphi> {...}

  AProcess:TProcess.Create(nil)
  {$IFDEF WIN32}
  AProcess.CommandLine := 'calc.exe'; //calculadora de Windows
  {$ENDIF}
  {$IFDEF LINUX}
  AProcess.CommandLine := 'kcalc'; //calculadora de KDE
  {$ENDIF}
  AProcess.Execute; //también puedes utilizar AProcess.Active:=True
{...}</delphi>

Ejemplo de conversación con el proceso aspell

   Dentro del código fuente pasdoc puedes encontrar dos unidades que realizan corrección ortográfica conversando con un proceso aspell a través de tuberías:

  • PasDoc_ProcessLineTalk.pas unit ejecuta la clase TProcessLineTalk, descendiente de TProcess, que se puede utilizar fácilmente para comunicar con cualquier proceso línea a línea.
  • PasDoc_Aspell.pas units ejecuta la clase TAspellProcess, que realiza corrección ortográfica utilizando la instancia fundamental TProcessLineTalk para ejecutar aspell y comunicarse con el proceso aspell que se está ejecutando.

   Ambas unidades son bastante independientes del resto de fuentes de pasdoc, así que pueden servir como ejemplos reales de la utilización de TProcess para ejecutar y comunicarse a través de tuberías con otros programas.

Reemplazar operadores de Shell como "|", "<" y ">"

   A veces queremos ejecutar una orden compleja que envía (entuba) sus datos a otra orden o a un archivo. Algo como

 ShellExecute('primeraorden.exe | segundaorden.exe');

o

 ShellExecute('dir > salida.txt');

    Esto no funciona con TProcess. p.e.:

 // esto no funciona
 Process.CommandLine := 'primeraorden.exe | segundaorden.exe'; 
 Process.Execute;

¿Por qué utilizar operadores especiales para redirigir la salida no funciona?

   TProcess es justamente, eso, un proceso, no es un entorno de shell. No es dos procesos, únicamente es un proceso. Es posible, no obstante, redirigir la salida de la manera que queremos. Ver más adelante, la siguiente sección.

Cómo redirigir la salida con TProcess

You can redirect the output of a command to another command by using a TProcess instance for each command.

Here's an example that explains how to redirect the output of one process to another. To redirect the output of a process to a file/stream see the example Reading Large Output <delphi>

 program Project1;
 
 uses
   Classes, sysutils, process;
 
 var
   FirstProcess,
   SecondProcess: TProcess;
   Buffer: array[0..127] of char;
   ReadCount: Integer;
   ReadSize: Integer;
 begin
   FirstProcess  := TProcess.Create(nil);
   SecondProcess := TProcess.Create(nil);
 
   FirstProcess.Options  := [poUsePipes];
   SecondProcess.Options := [poUsePipes,poStderrToOutPut];
 
   FirstProcess.CommandLine  := 'pwd';
   SecondProcess.CommandLine := 'grep '+ DirectorySeparator+ ' -';    
   // this would be the same as "pwd | grep / -"
 
   FirstProcess.Execute;
   SecondProcess.Execute;
 
   while FirstProcess.Running or (FirstProcess.Output.NumBytesAvailable > 0) do
   begin
     if FirstProcess.Output.NumBytesAvailable > 0 then
     begin
       // make sure that we don't read more data than we have allocated
       // in the buffer
       ReadSize := FirstProcess.Output.NumBytesAvailable;
       if ReadSize > SizeOf(Buffer) then
         ReadSize := SizeOf(Buffer);
       // now read the output into the buffer
       ReadCount := FirstProcess.Output.Read(Buffer[0], ReadSize);
       // and write the buffer to the second process
       SecondProcess.Input.Write(Buffer[0], ReadCount);
 
       // if SecondProcess writes much data to it's Output then 
       // we should read that data here to prevent a deadlock
       // see the previous example "Reading Large Output"
     end;
   end;
   // Close the input on the SecondProcess
   // so it finishes processing it's data
   SecondProcess.CloseInput;
 
   // and wait for it to complete
   // be carefull what command you run because it may not exit when
   // it's input is closed and the following line may loop forever
   while SecondProcess.Running do
     Sleep(1);
   // that's it! the rest of the program is just so the example
   // is a little 'useful'
 
   // we will reuse Buffer to output the SecondProcess's
   // output to *this* programs stdout
   WriteLn('Grep output Start:');
   ReadSize := SecondProcess.Output.NumBytesAvailable;
   if ReadSize > SizeOf(Buffer) then
     ReadSize := SizeOf(Buffer);
   if ReadSize > 0 then
   begin
     ReadCount := SecondProcess.Output.Read(Buffer, ReadSize);
     WriteLn(Copy(Buffer,0, ReadCount));
   end
   else
     WriteLn('grep did not find what we searched for. ', SecondProcess.ExitStatus);
   WriteLn('Grep output Finish:');
 
   // free our process objects
   FirstProcess.Free;
   SecondProcess.Free;
 end.

</delphi>

That's it. Now you can redirect output from one program to another.

Notes

This example may seem overdone since it's possible to run "complicated" commands using a shell with TProcess like:

 Process.Commandline := 'sh -c "pwd | grep / -"';

But our example is more crossplatform since it needs no modification to run on Windows or Linux etc. "sh" may or may not exist on your platform and is generally only available on *nix platforms. Also we have more flexibility in our example since you can read and write from/to the input, output and stderr of each process individually, which could be very advantageous for your project.