[fpc-devel] TProcess, bash, and escaping quotes

Seth Grover sethdgrover at gmail.com
Wed Feb 17 20:53:40 CET 2010


I'm working with TProcess, which for the most part I love, but I'm
scratching my head right now because of a problem I'm experiencing.

Suppose I have the following command:

ifconfig | grep -E -e "inet6? addr:" | grep -v -E -e
"(127.0.0.1|::1|Scope:Link)" | sed -e
"s/^.*addr:\s*\(\S*\)\(\/\d+\)*.*$/\1/g"

I can run that as-is from the command-line, since my shell hooks up
all of the pipes and whatnot for me. Also, I can execute it like this:

bash -c "ifconfig | grep -E -e \"inet6? addr:\" | grep -v -E -e
\"(127.0.0.1|::1|Scope:Link)\" | sed -e
\"s/^.*addr:\s*\(\S*\)\(\/\d+\)*.*$/\1/g\""

By escaping the double-quotes and then passing the whole thing to bash
as the argument for the -c, it works.

However, if I pass the above command (the one using bash -c with the
double-quotes of the original command escaped), as the CommandLine of
a TProcess, it doesn't work. TProcess execs bash, but the argument
doesn't seem to make it there correctly. Bash spits out:

$ ./tprocquotes
addr:\" | grep -v -E -e \"(127.0.0.1|::1|Scope:Link)\" | sed -e
\"s/^.*addr:\s*\(\S*\)\(\/\d+\)*.*$/\1/g\"": -c: line 0: unexpected
EOF while looking for matching `"'
addr:\" | grep -v -E -e \"(127.0.0.1|::1|Scope:Link)\" | sed -e
\"s/^.*addr:\s*\(\S*\)\(\/\d+\)*.*$/\1/g\"": -c: line 1: syntax error:
unexpected end of file

Yes, I know I could use single quotes instead of double quotes to the
-c argument, and then I wouldn't have to escape the double-quotes.
And, yes, I know there are other ways to accomplish what that command
does by parsing the output of ifconfig. I've got workarounds for this
particular example, but I still don't know why it shouldn't work with
TProcess with me escaping the double-quotes with a backslash and then
passing the whole thing double-quote enclosed to bash -c as the
TProcess CommandLine.

What am I doing wrong?

I've got an example program illustrating what I'm doing on PasteBin at
http://pastebin.com/f336d384b, and I'll also include the contents of
the program in this email in case anyone in the future's ever
searching the archives and the pastebin link doesn't exist any more.

-SG

-----------------------------------------------------------------------------------------------------------------
program tprocquotes;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  Classes,
  SysUtils,
  dateutils,
  process,
  pipes;

  function ExecuteProcess(const command : string;
                          out   commandExitCode : integer;
                          const timeoutSec : integer;
                          const stdout_stream : TStream;
                          const stderr_stream : TStream;
                          const redirectErr : boolean) : boolean;
  const
    READ_BYTES = 2048;
  var
   P : TProcess;
   buffer : array of byte;
   tmpStdOutput : TMemoryStream;
   tmpStdError : TMemoryStream;
   stdOutToUse : TStream;
   stdErrToUse : TStream;
   startTime : TDateTime;
   stdOutBytesRead : longword;

    function ReadAvailableBytes(const source_stream : TInputPipeStream;
                                const dest_stream : TStream) : longword;
    var
      bytes_available : longword;
      bytes_read      : longint;
      bytes_to_read   : longint;
    begin
      result := 0;
      if Assigned(source_stream) and Assigned(dest_stream) then begin
        bytes_available := source_stream.NumBytesAvailable;
        while (bytes_available > 0) do begin
          if (bytes_available >= READ_BYTES) then begin
            bytes_to_read := READ_BYTES;
          end else begin
            bytes_to_read := bytes_available;
          end;
          bytes_read := source_stream.Read(buffer[0], bytes_to_read);
          if (bytes_read > 0) then begin
            dest_stream.Write(buffer[0], bytes_read);
            result := result + longword(bytes_read);
          end;
          bytes_available := source_stream.NumBytesAvailable;
        end;
      end;
    end;

  begin

    result := true;
    commandExitCode := 1;

    if Assigned(stdout_stream) then begin
      tmpStdOutput := nil;
      stdOutToUse := stdout_stream;
    end else begin
      tmpStdOutput := TMemoryStream.Create;
      stdOutToUse := tmpStdOutput;
    end;
    stdOutToUse.Size := 0;
    stdOutToUse.Seek(0, soFromBeginning);

    if Assigned(stderr_stream) then begin
      tmpStdError := nil;
      stdErrToUse := stderr_stream;
    end else begin
      tmpStdError := TMemoryStream.Create;
      stdErrToUse := tmpStdError;
    end;
    stdErrToUse.Size := 0;
    stdErrToUse.Seek(0, soFromBeginning);

    SetLength(buffer, READ_BYTES);
    P := TProcess.Create(nil);
    try
      P.CommandLine := command;
      P.Options := [poUsePipes];
      if redirectErr then P.Options := P.Options + [poStderrToOutPut];
      P.InheritHandles := false;
      startTime := now;
      P.Execute;

      { read stdout while the process runs }
      while P.Running do begin

        stdOutBytesRead := ReadAvailableBytes(P.Output, stdOutToUse);
        if (stdOutBytesRead = 0) and
           (ReadAvailableBytes(P.Stderr, stdErrToUse) = 0) then begin
          sleep(100);
        end;

        { check timeout }
        if (timeoutSec > 0) and (SecondsBetween(now, startTime) >
timeoutSec) then begin
          result := false;
          P.Terminate(1);
          P.WaitOnExit; // avoid a zombie process
          break;
        end;
      end;

      commandExitCode := P.ExitStatus;

      { read what's left over in stdout }
      ReadAvailableBytes(P.Output, stdOutToUse);

      { read what's left over in stderr }
      ReadAvailableBytes(P.Stderr, stdErrToUse);

    finally
      if Assigned(tmpStdError) then FreeAndNil(tmpStdError);
      if Assigned(tmpStdOutput) then FreeAndNil(tmpStdOutput);
      FreeAndNil(P);
      SetLength(buffer, 0)
    end;

  end;

const
  COMMAND = 'bash -c "ifconfig | grep -E -e \"inet6? addr:\" | grep -v
-E -e \"(127.0.0.1|::1|Scope:Link)\" | sed -e
\"s/^.*addr:\s*\(\S*\)\(\/\d+\)*.*$/\1/g\""';
var
  stdOutStream : THandleStream;
  stdErrStream : THandleStream;
  commandExitCode : integer;
begin
  stdOutStream := THandleStream.Create(StdOutputHandle);
  stdErrStream := THandleStream.Create(StdErrorHandle);
  try
    if ExecuteProcess(COMMAND, commandExitCode, 120, stdOutStream,
stdErrStream, false) then begin
      ExitCode := commandExitCode;
    end else begin
      ExitCode := 1;
    end;
  finally
    FreeAndNil(stdOutStream);
    FreeAndNil(stdErrStream);
  end;
end.
-----------------------------------------------------------------------------------------------------------------

--
This email is fiction. Any resemblance to actual events
or persons living or dead is purely coincidental.

Seth Grover
sethdgrover[at]gmail[dot]com



More information about the fpc-devel mailing list